-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathimplementation_reference.html
More file actions
917 lines (842 loc) · 56.6 KB
/
implementation_reference.html
File metadata and controls
917 lines (842 loc) · 56.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MobAI'26 — Implementation Reference Guide</title>
<style>
:root {
--bg: #0d1117;
--card: #161b22;
--border: #30363d;
--text: #c9d1d9;
--heading: #f0f6fc;
--accent: #58a6ff;
--green: #3fb950;
--orange: #d29922;
--red: #f85149;
--purple: #bc8cff;
--cyan: #39d2c0;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
background: var(--bg); color: var(--text); line-height: 1.7;
padding: 20px; max-width: 1100px; margin: 0 auto;
}
h1 { color: var(--heading); font-size: 2em; margin-bottom: 8px; border-bottom: 2px solid var(--accent); padding-bottom: 10px; }
h2 { color: var(--accent); font-size: 1.5em; margin: 32px 0 12px; border-left: 4px solid var(--accent); padding-left: 12px; }
h3 { color: var(--green); font-size: 1.15em; margin: 20px 0 8px; }
h4 { color: var(--orange); font-size: 1em; margin: 14px 0 6px; }
p { margin: 8px 0; }
.subtitle { color: var(--purple); font-size: 0.95em; margin-bottom: 20px; }
.card {
background: var(--card); border: 1px solid var(--border); border-radius: 8px;
padding: 18px 22px; margin: 14px 0;
}
.highlight { background: #1f2937; border-left: 3px solid var(--accent); padding: 12px 16px; margin: 10px 0; border-radius: 4px; }
.formula {
background: #1a1e2e; border: 1px solid #2d3548; border-radius: 6px;
padding: 14px 18px; margin: 10px 0; text-align: center;
font-family: 'Courier New', monospace; font-size: 1.05em; color: var(--cyan);
}
code {
background: #1f2937; color: var(--cyan); padding: 2px 6px; border-radius: 3px;
font-family: 'Courier New', monospace; font-size: 0.9em;
}
pre {
background: #1a1e2e; border: 1px solid var(--border); border-radius: 6px;
padding: 14px; overflow-x: auto; margin: 10px 0;
font-family: 'Courier New', monospace; font-size: 0.85em; color: var(--text);
}
table { width: 100%; border-collapse: collapse; margin: 12px 0; }
th { background: #1f2937; color: var(--accent); text-align: left; padding: 10px 12px; border: 1px solid var(--border); font-weight: 600; }
td { padding: 8px 12px; border: 1px solid var(--border); }
tr:hover { background: #1f2937; }
.badge {
display: inline-block; padding: 3px 10px; border-radius: 12px;
font-size: 0.8em; font-weight: 600; margin: 2px;
}
.badge-green { background: #1a3a2a; color: var(--green); border: 1px solid var(--green); }
.badge-blue { background: #152238; color: var(--accent); border: 1px solid var(--accent); }
.badge-orange { background: #2a2010; color: var(--orange); border: 1px solid var(--orange); }
.badge-red { background: #2a1515; color: var(--red); border: 1px solid var(--red); }
.badge-purple { background: #1f1530; color: var(--purple); border: 1px solid var(--purple); }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.metric-box {
background: #1a1e2e; border: 1px solid var(--border); border-radius: 6px;
padding: 12px 16px; text-align: center;
}
.metric-val { font-size: 1.8em; font-weight: 700; color: var(--green); }
.metric-label { font-size: 0.85em; color: #8b949e; margin-top: 4px; }
.qa { margin: 10px 0; }
.qa-q { color: var(--orange); font-weight: 600; cursor: pointer; padding: 8px 0; }
.qa-q::before { content: "Q: "; color: var(--red); }
.qa-a { padding: 6px 0 6px 20px; border-left: 2px solid var(--border); margin-left: 4px; }
.qa-a::before { content: "A: "; color: var(--green); font-weight: 600; }
.toc { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 16px 22px; margin: 16px 0; }
.toc a { color: var(--accent); text-decoration: none; display: block; padding: 3px 0; }
.toc a:hover { text-decoration: underline; }
.flow-diagram {
background: #1a1e2e; border: 1px solid var(--border); border-radius: 8px;
padding: 20px; margin: 14px 0; font-family: 'Courier New', monospace;
font-size: 0.9em; white-space: pre-wrap;
}
.flow-box { display: inline-block; border: 2px solid var(--accent); border-radius: 6px; padding: 6px 14px; margin: 4px; }
.flow-arrow { color: var(--green); font-weight: bold; margin: 0 6px; }
details { margin: 8px 0; }
details summary { cursor: pointer; color: var(--accent); font-weight: 600; padding: 6px 0; }
details summary:hover { color: var(--green); }
.tag-cloud { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0; }
hr { border: none; border-top: 1px solid var(--border); margin: 24px 0; }
.note { background: #1c2333; border-left: 3px solid var(--orange); padding: 10px 14px; border-radius: 4px; margin: 10px 0; }
.note::before { content: "💡 "; }
.warn { background: #2a1a1a; border-left: 3px solid var(--red); padding: 10px 14px; border-radius: 4px; margin: 10px 0; }
.warn::before { content: "⚠️ "; }
@media (max-width: 700px) { .grid-2 { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<h1>MobAI'26 — Implementation Reference Guide</h1>
<p class="subtitle">Complete technical reference for algorithms, models, features, and architecture — for personal study & Q&A prep</p>
<div class="grid-2" style="margin: 20px 0;">
<div class="metric-box"><div class="metric-val">33.84%</div><div class="metric-label">10-day Aggregated WAPE</div></div>
<div class="metric-box"><div class="metric-val">0.9389</div><div class="metric-label">Classifier AUC</div></div>
<div class="metric-box"><div class="metric-val">+2.37%</div><div class="metric-label">10-day Bias</div></div>
<div class="metric-box"><div class="metric-val">155</div><div class="metric-label">Total Features</div></div>
</div>
<div class="toc">
<strong>Table of Contents</strong>
<a href="#arch">1. Overall Architecture</a>
<a href="#prophet">2. Prophet (Stage 0)</a>
<a href="#classifier">3. XGBoost Classifier (Stage 1)</a>
<a href="#regressor">4. XGBoost Regressor (Stage 2)</a>
<a href="#ensemble">5. Expected-Value Ensemble (Stage 3)</a>
<a href="#features">6. Feature Engineering (155 Features)</a>
<a href="#holidays">7. Algerian & Islamic Holidays</a>
<a href="#optimization">8. Warehouse Optimization (Task 1)</a>
<a href="#eval">9. Evaluation & Metrics</a>
<a href="#api">10. API Architecture</a>
<a href="#decisions">11. Design Decisions & Why</a>
<a href="#qa">12. Q&A — Common Questions</a>
<a href="#limits">13. Assumptions & Limitations</a>
</div>
<!-- ================================================================== -->
<h2 id="arch">1. Overall Architecture</h2>
<div class="card">
<h3>Three-Stage Ensemble Pipeline</h3>
<div class="flow-diagram">
<strong>Raw Data</strong> (historique_demande, 1129 SKUs, 674 days)
│
├──▶ <span style="color:var(--purple)">Stage 0: Prophet per-SKU (571 models)</span>
│ Linear growth, multiplicative seasonality
│ DZ/Islamic holidays, 5 temporal regressors
│ Output: mean_yhat, trend, weekly, yearly per product
│
├──▶ <span style="color:var(--accent)">Stage 1: XGBoost Classifier</span>
│ Binary: "Will this product have demand today?"
│ Output: P(demand > 0) — probability
│ AUC = 0.9389
│
├──▶ <span style="color:var(--green)">Stage 2: XGBoost Regressor</span>
│ Regression on demand-days only
│ Target: log1p(quantity)
│ Output: expm1(prediction)
│
└──▶ <span style="color:var(--orange)">Stage 3: Expected-Value Blending</span>
ŷ = P(demand)^3.0 × (0.725 × Prophet + 0.275 × Regressor) × 0.9
</div>
</div>
<div class="highlight">
<strong>Why 3 stages instead of 1 model?</strong><br>
The data is <strong>91% sparse</strong> (zeros). A single regression model would learn to predict near-zero for everything. By separating "will there be demand?" (classifier) from "how much?" (regressor), each model focuses on what it's good at. Prophet provides the temporal baseline that captures seasonality patterns the tree models miss.
</div>
<!-- ================================================================== -->
<h2 id="prophet">2. Prophet (Stage 0) — Time Series Baseline</h2>
<div class="card">
<h3>What is Prophet?</h3>
<p>Prophet is Facebook's <strong>additive/multiplicative decomposition model</strong> that decomposes time series into trend + seasonality + holidays + regressors. It's designed for business time series with strong seasonal effects.</p>
<div class="formula">y(t) = g(t) × (1 + s(t)) × (1 + h(t)) + ε</div>
<p style="text-align:center; font-size:0.85em; color:#8b949e;">g(t) = trend, s(t) = seasonality (multiplicative), h(t) = holiday effects</p>
</div>
<div class="card">
<h3>Configuration</h3>
<table>
<tr><th>Parameter</th><th>Value</th><th>Why</th></tr>
<tr><td><code>growth</code></td><td>linear</td><td>Demand doesn't grow exponentially — linear trend captures gradual market changes</td></tr>
<tr><td><code>seasonality_mode</code></td><td>multiplicative</td><td>Higher-demand products see proportionally larger seasonal swings, not additive offsets</td></tr>
<tr><td><code>yearly_seasonality</code></td><td>True</td><td>Strong yearly patterns (Ramadan, summer, back-to-school, etc.)</td></tr>
<tr><td><code>weekly_seasonality</code></td><td>True</td><td>Weekday/weekend demand differences (e.g., Friday prayer effect in Algeria)</td></tr>
<tr><td><code>daily_seasonality</code></td><td>False</td><td>Data is daily-aggregated — no sub-daily patterns to capture</td></tr>
<tr><td><code>changepoint_prior_scale</code></td><td>0.01</td><td>Conservative — prevents overfitting to short-term noise. Lower = smoother trend</td></tr>
<tr><td><code>seasonality_prior_scale</code></td><td>0.1</td><td>Regularized seasonality — prevents wild seasonal swings on sparse data</td></tr>
<tr><td><code>interval_width</code></td><td>0.95</td><td>95% prediction intervals for uncertainty quantification</td></tr>
<tr><td><code>holidays</code></td><td>DZ calendar</td><td>Algerian national + Islamic holidays (Ramadan, Eid, etc.) via <code>holidays_dz.py</code></td></tr>
</table>
</div>
<div class="card">
<h3>5 Temporal Regressors</h3>
<p>Added as external regressors to Prophet (all standardized):</p>
<table>
<tr><th>Regressor</th><th>Purpose</th></tr>
<tr><td><code>day_of_week</code></td><td>Captures intra-week patterns beyond basic weekly seasonality</td></tr>
<tr><td><code>is_weekend</code></td><td>Binary indicator — demand often drops on weekends</td></tr>
<tr><td><code>month</code></td><td>Captures monthly-level seasonality not fully modeled by yearly Fourier</td></tr>
<tr><td><code>is_month_start</code></td><td>Some products spike at month start (payroll-related purchasing)</td></tr>
<tr><td><code>is_month_end</code></td><td>Month-end restocking patterns</td></tr>
</table>
</div>
<div class="card">
<h3>Per-Product Calibration</h3>
<p>Prophet tends to under/over-predict for individual products because it uses a decomposition model that doesn't perfectly match demand levels.</p>
<div class="formula">cal_factor = mean(actual_demand_days) / mean(prophet_yhat_demand_days)</div>
<div class="formula">calibrated_yhat = yhat × clip(cal_factor, 0.2, 5.0)</div>
<p>This fixes systematic bias per product while preserving the temporal pattern.</p>
<div class="note">We clip between 0.2 and 5.0 to prevent extreme calibration factors from products with very few demand days.</div>
</div>
<div class="card">
<h3>Why Not Save Full Prophet Models?</h3>
<p>571 serialized Prophet objects ≈ <strong>~1 GB</strong>. Instead, we save <strong>metadata only</strong>:</p>
<ul>
<li><code>mean_yhat</code> — average calibrated prediction</li>
<li><code>trend_slope</code> — trend direction</li>
<li><code>cal_factor</code> — calibration scale</li>
<li><code>demand_freq</code> — how often product has demand</li>
</ul>
<p>At inference, we use <code>mean_yhat</code> as a static Prophet baseline. This trades ~5% accuracy for <strong>99.7% model size reduction</strong> (1 GB → 3 MB total).</p>
</div>
<div class="card">
<h3>Products Without Prophet</h3>
<p><strong>558 products</strong> have fewer than 10 demand-days — too sparse for Prophet to learn anything meaningful. For these, we use:</p>
<div class="formula">simple_baseline = mean(demand on days with demand > 0)</div>
<p>This is stored as a static value and used as the Prophet baseline for those products.</p>
</div>
<div class="highlight">
<strong>Why Prophet over ARIMA/SARIMA?</strong><br>
• Prophet handles irregular data (missing days) natively — ARIMA requires continuous series<br>
• Built-in holiday support — no manual intervention calendar<br>
• Multiplicative seasonality handles proportional effects better for heterogeneous products<br>
• Robust to outliers due to Bayesian framework with regularizing priors<br>
• Fast to fit: 571 models in ~15-20 minutes (ARIMA would take hours with auto-tuning)
</div>
<!-- ================================================================== -->
<h2 id="classifier">3. XGBoost Classifier (Stage 1)</h2>
<div class="card">
<h3>What it Does</h3>
<p>Predicts <strong>P(demand > 0 | features)</strong> — the probability that a product will have any demand on a given day. This is the most critical component because 91% of all product-days have zero demand.</p>
<p><span class="badge badge-green">AUC = 0.9389</span> on temporal test split</p>
</div>
<div class="card">
<h3>Why XGBoost?</h3>
<table>
<tr><th>Alternative</th><th>Why XGBoost wins</th></tr>
<tr><td>Logistic Regression</td><td>Can't capture non-linear feature interactions (lag × holiday × segment)</td></tr>
<tr><td>Random Forest</td><td>Slower, no native handling of imbalanced classes, less precise gradient descent</td></tr>
<tr><td>LightGBM</td><td>Similar performance, but XGBoost has better integration with our stack and more mature ecosystem</td></tr>
<tr><td>Neural Network</td><td>Overkill for tabular features, needs GPU, hard to interpret, 155 features work great with trees</td></tr>
<tr><td>CatBoost</td><td>Slower training, marginal improvement on numeric-heavy features (our categoricals are pre-encoded)</td></tr>
</table>
</div>
<div class="card">
<h3>Hyperparameters Explained</h3>
<table>
<tr><th>Param</th><th>Value</th><th>Why</th></tr>
<tr><td><code>objective</code></td><td>binary:logistic</td><td>Outputs probability P(demand > 0). Optimizes log-loss.</td></tr>
<tr><td><code>eval_metric</code></td><td>auc</td><td>AUC is better than accuracy for imbalanced datasets (91% zeros)</td></tr>
<tr><td><code>max_depth</code></td><td>7</td><td>Deep enough for complex interactions (lag × holiday × segment), not so deep it memorizes</td></tr>
<tr><td><code>learning_rate</code></td><td>0.04</td><td>Low LR + more rounds = better generalization than high LR + few rounds</td></tr>
<tr><td><code>subsample</code></td><td>0.8</td><td>Row subsampling — introduces randomness, reduces overfitting</td></tr>
<tr><td><code>colsample_bytree</code></td><td>0.6</td><td>Only 60% of features per tree — forces diversity, prevents reliance on single feature</td></tr>
<tr><td><code>colsample_bylevel</code></td><td>0.8</td><td>Additional column sampling at each tree level for finer regularization</td></tr>
<tr><td><code>min_child_weight</code></td><td>15</td><td>Minimum samples in leaf — prevents splits on tiny subsets (overfitting control)</td></tr>
<tr><td><code>scale_pos_weight</code></td><td>neg/pos ratio</td><td>≈ 10.1 — upweights rare positive class so model doesn't just predict all-zero</td></tr>
<tr><td><code>reg_alpha</code></td><td>0.5</td><td>L1 regularization — drives unimportant feature splits to zero</td></tr>
<tr><td><code>reg_lambda</code></td><td>3.0</td><td>L2 regularization — prevents any single tree from having extreme leaf values</td></tr>
<tr><td><code>gamma</code></td><td>0.1</td><td>Minimum loss reduction for split — prunes weak splits</td></tr>
<tr><td><code>tree_method</code></td><td>hist</td><td>Histogram-based splitting — 10x faster than exact, negligible accuracy loss</td></tr>
<tr><td><code>num_boost_round</code></td><td>800</td><td>With LR=0.04, needs many rounds. Early stopping at 50 prevents over-training.</td></tr>
</table>
</div>
<div class="highlight">
<strong>Key insight: scale_pos_weight</strong><br>
With 91% zeros, a naive model predicting "no demand" always gets 91% accuracy but is useless. <code>scale_pos_weight = neg_count / pos_count ≈ 10.1</code> tells XGBoost that each positive sample is worth ~10 negatives. This is equivalent to oversampling the minority class but more memory-efficient.
</div>
<!-- ================================================================== -->
<h2 id="regressor">4. XGBoost Regressor (Stage 2)</h2>
<div class="card">
<h3>What it Does</h3>
<p>Predicts the <strong>quantity of demand</strong> on days when demand occurs. Trained exclusively on demand-days (positive rows).</p>
<p>Target: <code>log1p(demand)</code> — Prediction: <code>expm1(output)</code></p>
</div>
<div class="card">
<h3>Why log1p Transform?</h3>
<p>Demand ranges from 1 to 10,000+. Without transform, the model optimizes for large values (high MSE) and ignores small orders.</p>
<div class="formula">log1p(x) = log(1 + x) ← maps [0, 10000] to [0, 9.2]</div>
<p>This <strong>compresses the range</strong>, making the model equally attentive to both small orders (1-10 units) and large orders (5000+ units). At prediction time, we reverse with <code>expm1()</code>.</p>
<div class="note"><strong>Why log1p not log?</strong> log(0) is undefined. log1p(0) = 0 — handles zeros gracefully (though we only train on positive rows).</div>
</div>
<div class="card">
<h3>Hyperparameters</h3>
<table>
<tr><th>Param</th><th>Value</th><th>Difference from Classifier</th></tr>
<tr><td><code>objective</code></td><td>reg:squarederror</td><td>Regression (MSE on log-scale) instead of classification</td></tr>
<tr><td><code>eval_metric</code></td><td>mae</td><td>MAE is more interpretable for demand quantities</td></tr>
<tr><td><code>max_depth</code></td><td>6</td><td>Slightly shallower — regression needs less interaction depth than classification</td></tr>
<tr><td><code>min_child_weight</code></td><td>10</td><td>Lower than classifier — regression on fewer samples needs more granularity</td></tr>
<tr><td><code>gamma</code></td><td>0.05</td><td>Lower threshold — allow finer splits for quantity prediction</td></tr>
</table>
<p>Other parameters (LR, subsample, colsample, reg) are same as classifier for consistency.</p>
</div>
<div class="highlight">
<strong>Why train on demand-days only?</strong><br>
If we trained on all 731K rows (91% zeros), the regressor would learn to predict near-zero for everything. By training only on the ~66K rows where demand > 0, it learns the <em>conditional distribution</em>: "Given that demand exists, how much?" The classifier handles the "does demand exist?" question.
</div>
<!-- ================================================================== -->
<h2 id="ensemble">5. Expected-Value Ensemble (Stage 3)</h2>
<div class="card">
<h3>The Final Prediction Formula</h3>
<div class="formula" style="font-size: 1.2em;">ŷ = P(demand)<sup>3.0</sup> × (0.725 × ŷ<sub>Prophet</sub> + 0.275 × ŷ<sub>Regressor</sub>) × 0.9</div>
<table style="margin-top: 14px;">
<tr><th>Component</th><th>Value</th><th>Role</th></tr>
<tr><td>P(demand)<sup>power</sup></td><td>power = 3.0</td><td>Sharpens probability — products with P<0.5 get heavily suppressed (0.5³ = 0.125), strong signals pass through (0.9³ = 0.729)</td></tr>
<tr><td>α (ensemble_alpha)</td><td>0.725</td><td>Prophet gets 72.5% weight — it captures temporal patterns better than the regressor for most products</td></tr>
<tr><td>1–α</td><td>0.275</td><td>Regressor gets 27.5% weight — it handles cross-product patterns and feature interactions</td></tr>
<tr><td>bias_multiplier</td><td>0.9</td><td>Global 10% scale-down — prevents systematic over-prediction (positive bias control)</td></tr>
</table>
</div>
<div class="card">
<h3>Expected-Value Mode vs Binary Threshold</h3>
<h4>Binary Threshold (traditional approach)</h4>
<pre>if P(demand) > threshold:
pred = α × Prophet + (1-α) × Regressor
else:
pred = 0</pre>
<p>❌ <strong>Problem:</strong> When aggregating over 10 days, binary decisions create "all-or-nothing" errors. A product with P=0.4 on 5 days gets 0 predicted demand total, even though expected demand = 0.4 × qty × 5 days.</p>
<h4>Expected-Value Mode (our approach)</h4>
<pre>pred = P(demand)^power × blended_quantity × bias_mult
# Always non-zero if P > 0 — scales continuously with probability</pre>
<p>✅ <strong>Advantage:</strong> For 10-day aggregated WAPE (the competition metric), expected-value mode produces better calibrated totals because it doesn't discard low-probability demand.</p>
<div class="note">This is mathematically equivalent to computing E[demand] = P(demand) × E[demand|demand>0], with power and bias as tuning knobs.</div>
</div>
<div class="card">
<h3>Tuning: 3D Grid Search</h3>
<p>Optimized on the temporal test set using 10-day aggregated WAPE:</p>
<ol>
<li><strong>Coarse search:</strong> α ∈ [0, 1] step 0.1 × bias ∈ [0.3, 3.5] step 0.1 × power ∈ {0.5, 0.75, 1.0, 1.5, 2.0, 3.0}</li>
<li><strong>Fine search:</strong> ±0.15 around best α (step 0.025) × ±0.3 around best bias (step 0.02) × ±0.3 around best power (step 0.05)</li>
</ol>
<p>Result: α=0.725, bias=0.9, power=3.0 → <strong>WAPE=33.84%</strong></p>
</div>
<div class="highlight">
<strong>Why prob_power = 3.0?</strong><br>
With power=1.0, a product with P=0.3 contributes 30% of its predicted quantity to the total. Over 10 days, this accumulates to significant phantom demand. With power=3.0, P=0.3 contributes only 2.7% — effectively filtering noise while keeping strong signals (P=0.8 → 51.2%). This acts as a <em>soft threshold</em> that's better than a hard cutoff for aggregated metrics.
</div>
<!-- ================================================================== -->
<h2 id="features">6. Feature Engineering (155 Features)</h2>
<div class="card">
<h3>Feature Categories</h3>
<table>
<tr><th>Category</th><th>Count</th><th>Features</th><th>Purpose</th></tr>
<tr>
<td><span class="badge badge-blue">Lag</span></td><td>10</td>
<td><code>lag_1, lag_2, lag_3, lag_7, lag_14, lag_21, lag_28, lag_w1, lag_w2, lag_w4</code></td>
<td>Recent demand history — the single most predictive feature category</td>
</tr>
<tr>
<td><span class="badge badge-green">Rolling Stats</span></td><td>~30</td>
<td><code>rmean_{3,7,14,28,60}, rstd, rmax, rmin, rmed, rsum</code></td>
<td>Smoothed demand level, volatility, and range at multiple horizons</td>
</tr>
<tr>
<td><span class="badge badge-purple">EWM</span></td><td>4</td>
<td><code>ewm_7, ewm_28, ewm7_norm, ewm28_norm</code></td>
<td>Exponentially weighted mean — recent days count more than old ones</td>
</tr>
<tr>
<td><span class="badge badge-orange">Demand Freq</span></td><td>5</td>
<td><code>dfreq_7/14/28/60, days_since</code></td>
<td>How regularly does this product sell? When was last sale?</td>
</tr>
<tr>
<td><span class="badge badge-purple">Prophet</span></td><td>12</td>
<td><code>prophet_yhat, trend, weekly, yearly, ratio, residuals, seasonal_str</code></td>
<td>Prophet's decomposition as features for XGBoost — brings temporal intelligence</td>
</tr>
<tr>
<td><span class="badge badge-blue">Calendar</span></td><td>~12</td>
<td><code>dow, month, week, dom, qtr, is_wknd, is_month_start/end, day_idx</code></td>
<td>Basic temporal features — day of week is among top importances</td>
</tr>
<tr>
<td><span class="badge badge-green">Fourier</span></td><td>16</td>
<td><code>fourier_sin/cos_y{1-4}, _w{1-2}, _m{1-2}</code></td>
<td>Smooth periodic features — helps trees capture cyclical patterns without hard splits</td>
</tr>
<tr>
<td><span class="badge badge-red">Holidays</span></td><td>20</td>
<td><code>is_ramadan, ramadan_progress, days_to_eid, is_national_holiday, ...</code></td>
<td>Algerian & Islamic calendar effects — Ramadan drives massive demand shifts</td></tr>
<tr>
<td><span class="badge badge-orange">Product</span></td><td>~15</td>
<td><code>p_total, p_freq, p_prio, cat_enc, poids_kg, volume_pcs, is_gerbable</code></td>
<td>Static product attributes — helps model group similar products</td></tr>
<tr>
<td><span class="badge badge-blue">Delivery</span></td><td>~10</td>
<td><code>del_rolling_7/14/28, del_qty_rolling, del_total_count</code></td>
<td>Supply-side signals — deliveries often precede demand spikes</td></tr>
<tr>
<td><span class="badge badge-purple">Interactions</span></td><td>~11</td>
<td><code>hf_x_dow, hf_x_lag1, hf_x_prophet, hf_x_ramadan, ...</code></td>
<td>Segment × feature interactions — HF products behave differently from LF</td></tr>
<tr>
<td><span class="badge badge-green">Normalized</span></td><td>~7</td>
<td><code>lag1_norm, rmean7_norm, rmean7_over_28, cv_28</code></td>
<td>Ratios relative to product average — enables cross-product comparison</td></tr>
</table>
</div>
<div class="card">
<h3>Key Feature Design Decisions</h3>
<h4>1. Lag Leakage Prevention</h4>
<p>All rolling features use <code>shift(1)</code> before rolling — we can never include today's value or future values.</p>
<pre>shifted = demand.groupby('id_produit').shift(1) # yesterday's demand
rmean_7 = shifted.rolling(7, min_periods=1).mean() # mean of last 7 days, excluding today</pre>
<h4>2. Same-Weekday Lags</h4>
<p><code>lag_w1</code> = demand exactly 7 days ago (same weekday). <code>lag_w2</code> = 14 days ago. <code>lag_w4</code> = 28 days ago.</p>
<p><code>rmean_wday4</code> = average of the 4 same-weekday lags — captures day-of-week-specific demand levels.</p>
<h4>3. Coefficient of Variation</h4>
<div class="formula">cv_28 = rstd_28 / (rmean_28 + 1e-6)</div>
<p>Measures demand volatility relative to level. High CV = erratic demand. Low CV = predictable.</p>
<h4>4. Prophet-as-Feature</h4>
<p>Instead of using Prophet predictions directly, we feed them as <strong>features to XGBoost</strong>. This lets XGBoost learn <em>when to trust Prophet</em> and <em>when to override it</em> based on other signals.</p>
<pre>prophet_ratio = prophet_yhat / prod_avg_demand # is Prophet above/below typical?
prophet_over_ewm7 = prophet_yhat / ewm_7 # Prophet vs recent trend
prophet_resid_lag1 = yesterday's (actual - Prophet) # was Prophet right yesterday?</pre>
<h4>5. 60-Day Warm-Up</h4>
<p>First 60 days are discarded after feature engineering because lag_28 + rolling_28 + shift(1) = 57 days of NaN propagation. We use <code>min_periods=1</code> but the first 60 days still have poor feature quality.</p>
</div>
<details>
<summary>Full list of all 155 features (click to expand)</summary>
<div class="tag-cloud" style="margin: 10px 0;">
<span class="badge badge-blue">cat_enc</span>
<span class="badge badge-blue">colisage_fardeau</span>
<span class="badge badge-blue">colisage_palette</span>
<span class="badge badge-green">cv_28</span>
<span class="badge badge-blue">day_idx</span>
<span class="badge badge-blue">day_idx_sq</span>
<span class="badge badge-orange">days_since</span>
<span class="badge badge-red">days_since_ramadan</span>
<span class="badge badge-red">days_to_eid_adha</span>
<span class="badge badge-red">days_to_eid_fitr</span>
<span class="badge badge-red">days_to_national_holiday</span>
<span class="badge badge-red">days_to_ramadan</span>
<span class="badge badge-blue">del_avg_qty</span>
<span class="badge badge-blue">del_n_days</span>
<span class="badge badge-blue">del_qty_rolling_28</span>
<span class="badge badge-blue">del_qty_rolling_7</span>
<span class="badge badge-blue">del_rolling_14</span>
<span class="badge badge-blue">del_rolling_28</span>
<span class="badge badge-blue">del_rolling_7</span>
<span class="badge badge-blue">del_total_count</span>
<span class="badge badge-blue">del_total_qty</span>
<span class="badge badge-orange">dfreq_14</span>
<span class="badge badge-orange">dfreq_28</span>
<span class="badge badge-orange">dfreq_60</span>
<span class="badge badge-orange">dfreq_7</span>
<span class="badge badge-blue">dom</span>
<span class="badge badge-blue">dow</span>
<span class="badge badge-red">eid_adha_prep</span>
<span class="badge badge-red">eid_fitr_prep</span>
<span class="badge badge-purple">ewm28_norm</span>
<span class="badge badge-purple">ewm7_norm</span>
<span class="badge badge-purple">ewm_28</span>
<span class="badge badge-purple">ewm_7</span>
<span class="badge badge-green">fourier_cos_m1</span>
<span class="badge badge-green">fourier_cos_m2</span>
<span class="badge badge-green">fourier_cos_w1</span>
<span class="badge badge-green">fourier_cos_w2</span>
<span class="badge badge-green">fourier_cos_y1</span>
<span class="badge badge-green">fourier_cos_y2</span>
<span class="badge badge-green">fourier_cos_y3</span>
<span class="badge badge-green">fourier_cos_y4</span>
<span class="badge badge-green">fourier_sin_m1</span>
<span class="badge badge-green">fourier_sin_m2</span>
<span class="badge badge-green">fourier_sin_w1</span>
<span class="badge badge-green">fourier_sin_w2</span>
<span class="badge badge-green">fourier_sin_y1</span>
<span class="badge badge-green">fourier_sin_y2</span>
<span class="badge badge-green">fourier_sin_y3</span>
<span class="badge badge-green">fourier_sin_y4</span>
<span class="badge badge-purple">hf_x_days_since</span>
<span class="badge badge-purple">hf_x_dfreq7</span>
<span class="badge badge-purple">hf_x_dow</span>
<span class="badge badge-purple">hf_x_eid_adha</span>
<span class="badge badge-purple">hf_x_eid_fitr</span>
<span class="badge badge-purple">hf_x_is_wknd</span>
<span class="badge badge-purple">hf_x_lag1</span>
<span class="badge badge-purple">hf_x_month</span>
<span class="badge badge-purple">hf_x_prophet</span>
<span class="badge badge-purple">hf_x_ramadan</span>
<span class="badge badge-purple">hf_x_ramadan_prep</span>
<span class="badge badge-purple">hf_x_rmean7</span>
<span class="badge badge-red">is_any_holiday</span>
<span class="badge badge-red">is_ashura</span>
<span class="badge badge-red">is_eid_adha</span>
<span class="badge badge-red">is_eid_fitr</span>
<span class="badge badge-blue">is_gerbable</span>
<span class="badge badge-blue">is_hf</span>
<span class="badge badge-red">is_islamic_new_year</span>
<span class="badge badge-red">is_mawlid</span>
<span class="badge badge-blue">is_month_end</span>
<span class="badge badge-blue">is_month_start</span>
<span class="badge badge-red">is_national_holiday</span>
<span class="badge badge-red">is_ramadan</span>
<span class="badge badge-blue">is_week_start</span>
<span class="badge badge-blue">is_wknd</span>
<span class="badge badge-green">lag1_norm</span>
<span class="badge badge-blue">lag_1</span>
<span class="badge badge-blue">lag_14</span>
<span class="badge badge-blue">lag_2</span>
<span class="badge badge-blue">lag_21</span>
<span class="badge badge-blue">lag_28</span>
<span class="badge badge-blue">lag_3</span>
<span class="badge badge-blue">lag_7</span>
<span class="badge badge-blue">lag_w1</span>
<span class="badge badge-blue">lag_w2</span>
<span class="badge badge-blue">lag_w4</span>
<span class="badge badge-blue">month</span>
<span class="badge badge-blue">p_avg</span>
<span class="badge badge-blue">p_days</span>
<span class="badge badge-blue">p_demand_score</span>
<span class="badge badge-blue">p_freq</span>
<span class="badge badge-blue">p_freq_score</span>
<span class="badge badge-blue">p_prio</span>
<span class="badge badge-blue">p_total</span>
<span class="badge badge-blue">poids_kg</span>
<span class="badge badge-red">pre_holiday_3d</span>
<span class="badge badge-green">prod_avg_demand</span>
<span class="badge badge-green">prod_med_demand</span>
<span class="badge badge-green">prod_n_days</span>
<span class="badge badge-green">prod_std_demand</span>
<span class="badge badge-purple">prophet_over_ewm7</span>
<span class="badge badge-purple">prophet_over_rmean7</span>
<span class="badge badge-purple">prophet_ratio</span>
<span class="badge badge-purple">prophet_resid_lag1</span>
<span class="badge badge-purple">prophet_resid_rmean7</span>
<span class="badge badge-purple">prophet_seasonal_str</span>
<span class="badge badge-purple">prophet_trend</span>
<span class="badge badge-purple">prophet_trend_norm</span>
<span class="badge badge-purple">prophet_weekly</span>
<span class="badge badge-purple">prophet_weekly_abs</span>
<span class="badge badge-purple">prophet_yearly</span>
<span class="badge badge-purple">prophet_yearly_abs</span>
<span class="badge badge-purple">prophet_yhat</span>
<span class="badge badge-blue">qtr</span>
<span class="badge badge-red">ramadan_day</span>
<span class="badge badge-red">ramadan_last_week</span>
<span class="badge badge-red">ramadan_prep</span>
<span class="badge badge-red">ramadan_progress</span>
<span class="badge badge-green">rmax_14</span>
<span class="badge badge-green">rmax_28</span>
<span class="badge badge-green">rmax_7</span>
<span class="badge badge-green">rmean28_norm</span>
<span class="badge badge-green">rmean7_norm</span>
<span class="badge badge-green">rmean7_over_28</span>
<span class="badge badge-green">rmean_14</span>
<span class="badge badge-green">rmean_28</span>
<span class="badge badge-green">rmean_3</span>
<span class="badge badge-green">rmean_60</span>
<span class="badge badge-green">rmean_7</span>
<span class="badge badge-green">rmean_wday4</span>
<span class="badge badge-green">rmean_wday4_norm</span>
<span class="badge badge-green">rmed_14</span>
<span class="badge badge-green">rmed_28</span>
<span class="badge badge-green">rmed_7</span>
<span class="badge badge-green">rmin_14</span>
<span class="badge badge-green">rmin_28</span>
<span class="badge badge-green">rmin_7</span>
<span class="badge badge-green">rstd_14</span>
<span class="badge badge-green">rstd_28</span>
<span class="badge badge-green">rstd_3</span>
<span class="badge badge-green">rstd_60</span>
<span class="badge badge-green">rstd_7</span>
<span class="badge badge-green">rsum_14</span>
<span class="badge badge-green">rsum_28</span>
<span class="badge badge-green">rsum_7</span>
<span class="badge badge-blue">volume_pcs</span>
<span class="badge badge-blue">week</span>
</div>
</details>
<!-- ================================================================== -->
<h2 id="holidays">7. Algerian & Islamic Holidays</h2>
<div class="card">
<h3>Why Holidays Matter</h3>
<p>Algeria follows both the Gregorian and Islamic (Hijri) calendars. Islamic holidays shift ~11 days earlier each Gregorian year, creating moving demand patterns that are invisible to standard seasonality models.</p>
<p><strong>Ramadan</strong> is the biggest demand driver — food and beverage consumption patterns change dramatically during the fasting month. Pre-Ramadan stocking, Eid celebrations, and post-Eid normalization create a complex demand wave spanning ~50 days.</p>
</div>
<div class="card">
<h3>20 Holiday Features</h3>
<table>
<tr><th>Feature</th><th>Type</th><th>Description</th></tr>
<tr><td><code>is_ramadan</code></td><td>Binary</td><td>1 if date falls within Ramadan (30 days)</td></tr>
<tr><td><code>ramadan_progress</code></td><td>0→1</td><td>How far into Ramadan (0 = day 1, 1 = day 30)</td></tr>
<tr><td><code>ramadan_day</code></td><td>1-30</td><td>Day number within Ramadan</td></tr>
<tr><td><code>ramadan_prep</code></td><td>Binary</td><td>15 days before Ramadan — stocking period</td></tr>
<tr><td><code>ramadan_last_week</code></td><td>Binary</td><td>Last 7 days of Ramadan — Eid preparation</td></tr>
<tr><td><code>days_to_ramadan</code></td><td>Float</td><td>Days until next Ramadan start</td></tr>
<tr><td><code>days_since_ramadan</code></td><td>Float</td><td>Days since Ramadan ended</td></tr>
<tr><td><code>is_eid_fitr</code></td><td>Binary</td><td>Eid al-Fitr (end of Ramadan) — 3-day holiday</td></tr>
<tr><td><code>eid_fitr_prep</code></td><td>Binary</td><td>3 days before Eid al-Fitr</td></tr>
<tr><td><code>days_to_eid_fitr</code></td><td>Float</td><td>Days until next Eid al-Fitr</td></tr>
<tr><td><code>is_eid_adha</code></td><td>Binary</td><td>Eid al-Adha — 3-day holiday</td></tr>
<tr><td><code>eid_adha_prep</code></td><td>Binary</td><td>3 days before Eid al-Adha</td></tr>
<tr><td><code>days_to_eid_adha</code></td><td>Float</td><td>Days until next Eid al-Adha</td></tr>
<tr><td><code>is_mawlid</code></td><td>Binary</td><td>Prophet's Birthday</td></tr>
<tr><td><code>is_islamic_new_year</code></td><td>Binary</td><td>1st Muharram</td></tr>
<tr><td><code>is_ashura</code></td><td>Binary</td><td>10th Muharram</td></tr>
<tr><td><code>is_national_holiday</code></td><td>Binary</td><td>Independence Day (Jul 5), Revolution (Nov 1), etc.</td></tr>
<tr><td><code>days_to_national_holiday</code></td><td>Float</td><td>Days until next national holiday</td></tr>
<tr><td><code>is_any_holiday</code></td><td>Binary</td><td>Any holiday (union of all above)</td></tr>
<tr><td><code>pre_holiday_3d</code></td><td>Binary</td><td>3 days before any major holiday</td></tr>
</table>
</div>
<div class="card">
<h3>Hijri Calendar Approximation</h3>
<p>Islamic dates are computed using a solar-to-lunar conversion formula:</p>
<pre>hijri_year ≈ (gregorian_year - 622) × (33/32)
islamic_day = (date - epoch).days
hijri_day_in_year = islamic_day % 354.36667</pre>
<p>Pre-computed for 2023–2028. Accuracy: ±1 day (acceptable for feature engineering).</p>
</div>
<div class="card">
<h3>Segment × Holiday Interactions</h3>
<p>HF products behave differently during holidays than LF products. We explicitly model this:</p>
<table>
<tr><th>Interaction</th><th>Intuition</th></tr>
<tr><td><code>hf_x_ramadan</code></td><td>HF food products spike during Ramadan; LF products may not</td></tr>
<tr><td><code>hf_x_eid_fitr</code></td><td>HF celebration products surge for Eid</td></tr>
<tr><td><code>hf_x_eid_adha</code></td><td>Sacrifice holiday affects meat/food products differently</td></tr>
<tr><td><code>hf_x_ramadan_prep</code></td><td>Stocking behavior for high-frequency items pre-Ramadan</td></tr>
</table>
</div>
<!-- ================================================================== -->
<h2 id="optimization">8. Warehouse Optimization (Task 1)</h2>
<div class="card">
<h3>Approach: AI-Guided Heuristic</h3>
<div class="flow-diagram">
<strong>Input Event:</strong> (Date, Product ID, Flow Type, Quantity)
│
├── <span style="color:var(--green)">Ingoing (Reception → Storage)</span>
│ 1. Look up product segment (HF/LF)
│ 2. HF → target PICKING zone | LF → target RESERVE zone
│ 3. Score available slots by distance + floor
│ 4. Assign best slot, record route
│
└── <span style="color:var(--orange)">Outgoing (Storage → Expedition)</span>
1. Find stored slot for product
2. Generate picking route
3. Release slot
4. Track zone congestion
<strong>Output:</strong> (Product, Action, Location, Route, Reason)
</div>
</div>
<div class="card">
<h3>Product Segmentation</h3>
<p>Products are classified into <strong>HF (High Frequency)</strong> and <strong>LF (Low Frequency)</strong> based on demand analysis:</p>
<div class="formula">priority_score = 0.5 × demand_score + 0.5 × frequency_score</div>
<p>Where <code>demand_score = total_demand / max_total_demand</code> and <code>frequency_score = demand_frequency / max_frequency</code></p>
<p>Top 20% by priority score → HF. Result: <strong>233 HF products, 896 LF products</strong></p>
<div class="note"><strong>Pareto principle:</strong> ~20% of products (HF) generate ~80% of warehouse movements. Placing them optimally has the highest impact.</div>
</div>
<div class="card">
<h3>Slot Scoring Function</h3>
<div class="formula">score = −w<sub>d</sub> × dist_expedition − w<sub>z</sub> × z_level − w<sub>h</sub> × z_level × is_heavy</div>
<table>
<tr><th>Segment</th><th>w<sub>d</sub> (distance)</th><th>w<sub>z</sub> (floor)</th><th>w<sub>h</sub> (heavy)</th><th>Logic</th></tr>
<tr><td><span class="badge badge-green">HF</span></td><td>3.0</td><td>5.0</td><td>10.0 if >5kg</td><td>Close to expedition + ground floor = fastest access</td></tr>
<tr><td><span class="badge badge-blue">LF</span></td><td>1.0</td><td>2.0</td><td>10.0 if >5kg</td><td>Less critical — can be further away or at height</td></tr>
</table>
<p>Negative scores: the <strong>highest (least negative) score wins</strong> — closest slot with lowest floor.</p>
</div>
<div class="card">
<h3>Route Descriptions</h3>
<table>
<tr><th>Zone Pattern</th><th>Ingoing Route</th><th>Outgoing Route</th></tr>
<tr><td>PCK (Picking)</td><td>Reception → Zone Picking B7 → Slot</td><td>Slot → Zone Picking B7 → Expedition</td></tr>
<tr><td>B07-N (Level N)</td><td>Reception → Lift → Level N → Slot</td><td>Slot → Level N → Lift → Expedition</td></tr>
<tr><td>B07-SS (Sous-sol)</td><td>Reception → Lift → Sous-sol → Slot</td><td>Slot → Sous-sol → Lift → Expedition</td></tr>
</table>
</div>
<div class="card">
<h3>Congestion Management</h3>
<p>The simulation tracks <strong>zone visits per day</strong>. When a zone exceeds 5 operations/day, a congestion warning is generated. This simulates real warehouse bottlenecks:</p>
<ul>
<li>Multiple forklift operations in the same aisle</li>
<li>Lift contention for multi-floor operations</li>
<li>Reception/expedition zone saturation during peak hours</li>
</ul>
</div>
<!-- ================================================================== -->
<h2 id="eval">9. Evaluation & Metrics</h2>
<div class="card">
<h3>Primary Metric: 10-Day Aggregated WAPE</h3>
<p>WAPE = Weighted Absolute Percentage Error, computed on 10-day product-level aggregates:</p>
<div class="formula">WAPE = Σ |actual_10d − predicted_10d| / Σ actual_10d × 100%</div>
<p>Where each sum is over all (product, 10-day-window) pairs with actual > 0.</p>
<h4>Why 10-day aggregated (not daily)?</h4>
<ul>
<li>Daily prediction for sparse products is nearly impossible (91% zeros)</li>
<li>Business value: warehouse planning works on multi-day horizons</li>
<li>Aggregation smooths daily noise — rewards models that get the right <em>total</em>, even if individual days are off</li>
</ul>
</div>
<div class="card">
<h3>Results Comparison</h3>
<table>
<tr><th>Method</th><th>10-day WAPE</th><th>10-day Bias</th><th>Daily dd WAPE</th><th>Notes</th></tr>
<tr><td>Lag-1 baseline</td><td>31.1%</td><td>—</td><td>108.2%</td><td>Yesterday's demand repeated. Hard to beat on aggregated metric.</td></tr>
<tr><td>Prophet-only</td><td>89.2%</td><td>—</td><td>74.7%</td><td>Good at daily patterns but bad at 10-day aggregation (systematic over-prediction)</td></tr>
<tr><td>Regressor-only</td><td>54.0%</td><td>—</td><td>—</td><td>Missing zero-demand information hurts aggregation</td></tr>
<tr><td>EWM(7) baseline</td><td>—</td><td>—</td><td>83.1%</td><td>Simple exponential smoothing</td></tr>
<tr style="background: #1a3a2a;"><td><strong>Our Ensemble (EV mode)</strong></td><td><strong>33.84%</strong></td><td><strong>+2.37%</strong></td><td><strong>72.3%</strong></td><td><strong>Best AI model. Bias near-zero = well-calibrated.</strong></td></tr>
</table>
<div class="warn">Lag-1 has slightly better 10-day WAPE (31.1% vs 33.8%) because it exploits short-term autocorrelation. However, it has no forecasting ability — it just repeats yesterday. Our model provides genuine forward-looking predictions.</div>
</div>
<div class="card">
<h3>Classifier Metrics</h3>
<table>
<tr><th>Metric</th><th>Value</th><th>Interpretation</th></tr>
<tr><td>AUC-ROC</td><td>0.9389</td><td>93.89% chance of ranking a random positive above a random negative</td></tr>
<tr><td>Class imbalance</td><td>91% negative</td><td>Only 9% of product-days have demand — extreme sparsity</td></tr>
<tr><td>Training samples</td><td>731,592</td><td>Full grid (products × days) minus 60-day warm-up</td></tr>
<tr><td>Test samples</td><td>33,870</td><td>Last 30 days of data</td></tr>
</table>
</div>
<!-- ================================================================== -->
<h2 id="api">10. API Architecture</h2>
<div class="card">
<h3>Tech Stack</h3>
<table>
<tr><th>Component</th><th>Technology</th><th>Why</th></tr>
<tr><td>Backend</td><td>FastAPI + Uvicorn</td><td>Async, fast, auto-documentation (OpenAPI), type validation (Pydantic)</td></tr>
<tr><td>ML Runtime</td><td>XGBoost Booster API</td><td>Direct C++ inference — no sklearn overhead, ~1ms per prediction</td></tr>
<tr><td>Frontend</td><td>Single-file HTML/CSS/JS</td><td>No build step, served from <code>/</code>, works anywhere</td></tr>
<tr><td>Deployment</td><td>Azure VM (Standard B2s)</td><td>2 vCPU, 4 GB RAM — sufficient for XGBoost CPU inference</td></tr>
</table>
</div>
<div class="card">
<h3>12 Endpoints</h3>
<table>
<tr><th>Endpoint</th><th>Method</th><th>Purpose</th></tr>
<tr><td><code>/predict</code></td><td>POST</td><td>Single product-date demand prediction</td></tr>
<tr><td><code>/generate-forecast</code></td><td>POST</td><td>Batch: all products × date range → CSV</td></tr>
<tr><td><code>/explain</code></td><td>POST</td><td>Full XAI breakdown of prediction</td></tr>
<tr><td><code>/model-info</code></td><td>GET</td><td>Model metadata, performance stats</td></tr>
<tr><td><code>/assign-storage</code></td><td>POST</td><td>Optimal slot assignment for incoming product</td></tr>
<tr><td><code>/optimize-picking</code></td><td>POST</td><td>Generate picking route for outgoing</td></tr>
<tr><td><code>/simulate-warehouse</code></td><td>POST</td><td>Full simulation: process events chronologically</td></tr>
<tr><td><code>/preparation-order</code></td><td>POST</td><td>Generate preparation order based on forecast</td></tr>
<tr><td><code>/warehouse-state</code></td><td>GET</td><td>Current warehouse occupancy</td></tr>
<tr><td><code>/reset-warehouse</code></td><td>POST</td><td>Reset to empty state</td></tr>
<tr><td><code>/health</code></td><td>GET</td><td>Health check + model status</td></tr>
<tr><td><code>/download/{filename}</code></td><td>GET</td><td>Download generated CSV files</td></tr>
</table>
</div>
<!-- ================================================================== -->
<h2 id="decisions">11. Design Decisions & Why</h2>
<div class="card">
<div class="qa">
<div class="qa-q">Why Prophet + XGBoost and not just one model?</div>
<div class="qa-a">Prophet captures <strong>temporal decomposition</strong> (trend, weekly, yearly, holidays) that tree models struggle with because trees make axis-aligned splits — they can't naturally learn sin/cos patterns. XGBoost captures <strong>cross-product patterns and feature interactions</strong> that Prophet (per-SKU) misses. The ensemble gets the best of both worlds.</div>
</div>
<div class="qa">
<div class="qa-q">Why not LSTM / Transformer / DeepAR?</div>
<div class="qa-a"><strong>1)</strong> Tabular data with engineered features works better with gradient boosting than deep learning in most Kaggle competitions. <strong>2)</strong> Deep models need GPU and longer training — impractical for hackathon timeline. <strong>3)</strong> 155 engineered features capture the same information that an LSTM would learn automatically, but with full interpretability. <strong>4)</strong> Our total model size is 3 MB — a Transformer would be 100x larger.</div>
</div>
<div class="qa">
<div class="qa-q">Why multiplicative seasonality in Prophet?</div>
<div class="qa-a">A product selling 1000 units/day might see +500 during Ramadan (+50%). A product selling 10 units/day would see +5 (+50%). Multiplicative mode applies the same <em>percentage</em> change regardless of level. Additive mode would apply the same <em>absolute</em> change (+500 to both), which is wrong for low-demand products.</div>
</div>
<div class="qa">
<div class="qa-q">Why separate classifier and regressor (not multi-output)?</div>
<div class="qa-a"><strong>Different objectives and training data.</strong> The classifier sees ALL 731K rows (91% zeros) — its job is discrimination. The regressor sees only ~66K positive rows — its job is quantity estimation. If you train a single model on all rows with MSE loss, it optimizes for predicting zeros (since they're 91% of the loss), not for accurate quantity estimation on demand-days.</div>
</div>
<div class="qa">
<div class="qa-q">Why log1p transform and not just raw demand?</div>
<div class="qa-a">Demand ranges from 1 to 10,000+. MSE loss would make the model focus on getting big orders right (high squared error) and ignore small orders. log1p compresses the range to [0, 9.2], so a 50% error on a 10-unit order counts similarly to a 50% error on a 5000-unit order. This improves WAPE, which also treats all products proportionally.</div>
</div>
<div class="qa">
<div class="qa-q">Why 60-day warm-up period?</div>
<div class="qa-a">lag_28 needs 28 days of history. Rolling stats with shift(1) need window + 1 days. Some features need 60 days to be fully populated. While we use <code>min_periods=1</code>, the quality of features in the first 60 days is poor (based on very few data points), which would hurt model training.</div>
</div>
<div class="qa">
<div class="qa-q">Why HF/LF segmentation for warehouse optimization?</div>
<div class="qa-a">Pareto principle: ~20% of products account for ~80% of warehouse movements. By placing these HF products in PICKING zone (ground floor, near expedition), we minimize travel time for the majority of operations. LF products are rarely accessed, so storing them in RESERVE (further, higher floors) is acceptable.</div>
</div>
</div>
<!-- ================================================================== -->
<h2 id="qa">12. Q&A — Common Questions</h2>
<div class="card">
<div class="qa">
<div class="qa-q">What is WAPE and why use it instead of MAPE?</div>
<div class="qa-a"><strong>WAPE</strong> = Weighted Absolute Percentage Error = Σ|actual-pred| / Σ actual. It's a <em>volume-weighted</em> average — products with higher demand contribute more to the metric. <strong>MAPE</strong> (Mean APE per product) treats a 50% error on a 1-unit product equally to a 50% error on a 10,000-unit product. WAPE is more business-relevant because high-volume products matter more for warehouse planning.</div>
</div>
<div class="qa">
<div class="qa-q">What does AUC = 0.9389 mean in practice?</div>
<div class="qa-a">If you randomly pick one product-day with demand and one without, there's a 93.89% chance the classifier assigns a higher probability to the demand day. It means the model is excellent at distinguishing "will sell" vs "won't sell" — critical for a dataset where 91% of entries are zero.</div>
</div>
<div class="qa">
<div class="qa-q">How does the model handle new products with no history?</div>
<div class="qa-a">Cold-start products get: lag features = 0, rolling stats = 0, Prophet baseline = 0 (no model fitted), product attributes still available. The classifier will output low P(demand), and the regressor will rely on product attributes and calendar features. Prediction quality will be poor until history accumulates.</div>
</div>
<div class="qa">
<div class="qa-q">What's the computational cost?</div>
<div class="qa-a"><strong>Training:</strong> ~20 min for Prophet (571 models) + ~5 min for XGBoost (classifier + regressor) + ~3 min for grid search = ~30 min total on CPU. <strong>Inference:</strong> ~25ms per product-date (feature construction dominates). 1129 products × 1 day ≈ 30 seconds.</div>
</div>
<div class="qa">
<div class="qa-q">Why is bias +2.37% (slight over-prediction)?</div>
<div class="qa-a">The bias_multiplier = 0.9 was tuned to balance WAPE and bias. A slight positive bias means we predict slightly more than actual — preferable in warehouse management because under-stocking causes stockouts (lost sales, customer dissatisfaction), while over-stocking just means slightly more inventory cost.</div>
</div>
<div class="qa">
<div class="qa-q">What are Fourier features and why use them?</div>
<div class="qa-a">Fourier features are sin/cos transformations of day-of-year, day-of-week, and day-of-month: <code>sin(2πk×day/365)</code>. Trees make axis-aligned splits, so they can't natively learn "demand peaks in March and September." Fourier features create smooth variables that trees can split on to approximate cyclical patterns. We use k=1,2,3,4 for yearly (captures complex multi-modal year patterns), k=1,2 for weekly and monthly.</div>
</div>
<div class="qa">
<div class="qa-q">Why not use deep ensembles or stacking?</div>
<div class="qa-a">We effectively <em>do</em> stack: Prophet → features → XGBoost. Adding more layers (e.g., a meta-learner on top) risks overfitting on the small test set. The 3D grid search over (alpha, bias, power) acts as our meta-learner, but with only 3 parameters — much less risk of overfitting than a learned stacking layer.</div>
</div>
<div class="qa">
<div class="qa-q">How do you prevent data leakage?</div>
<div class="qa-a"><strong>1)</strong> Temporal split (train < 2025-12-09, test > 2025-12-10) — never test on data the model has seen. <strong>2)</strong> All lag/rolling features use <code>shift(1)</code> — never include today's demand. <strong>3)</strong> Prophet is fitted on training data only. <strong>4)</strong> Product stats (avg, frequency) computed on training period. <strong>5)</strong> No future-looking features (Prophet might seem to, but we only use in-sample fitted values, not forward predictions, during training).</div>
</div>
<div class="qa">
<div class="qa-q">What is the Expected-Value approach mathematically?</div>
<div class="qa-a">It's based on the law of total expectation: E[Y] = P(Y>0) × E[Y|Y>0]. The classifier gives P(Y>0), the blended Prophet+Regressor gives E[Y|Y>0]. The prob_power and bias_mult are calibration parameters. Power>1 acts as a soft threshold: values close to 0 get pushed further down (0.3³=0.027), while values close to 1 barely change (0.9³=0.729). This effectively filters noise without the discontinuity of a hard threshold.</div>
</div>
<div class="qa">
<div class="qa-q">Why XGBoost over LightGBM?</div>
<div class="qa-a">Performance-wise they're nearly identical for this dataset. We chose XGBoost because: <strong>1)</strong> Native JSON model format (easy to serialize/load). <strong>2)</strong> More mature Booster API for direct low-level inference. <strong>3)</strong> Slightly better documentation for custom objectives. <strong>4)</strong> Team familiarity. In production, LightGBM would work just as well.</div>
</div>
</div>
<!-- ================================================================== -->
<h2 id="limits">13. Assumptions & Limitations</h2>
<div class="card">
<table>
<tr><th>#</th><th>Limitation</th><th>Impact</th><th>Potential Fix</th></tr>
<tr><td>1</td><td>Prophet metadata vs full models</td><td>~5% accuracy loss — we use mean_yhat instead of date-specific predictions at inference</td><td>Save full Prophet objects (would increase model size to ~1 GB)</td></tr>
<tr><td>2</td><td>Islamic date ±1 day accuracy</td><td>Holiday features may be off by 1 day</td><td>Use observed dates from official calendar (requires annual updates)</td></tr>
<tr><td>3</td><td>Empty initial warehouse state</td><td>Simulation starts empty — first outgoing events show stockout</td><td>Load initial state from database/file</td></tr>
<tr><td>4</td><td>91% data sparsity</td><td>Daily per-product WAPE is high (72.3%) — most value in aggregated predictions</td><td>Product grouping (pool similar products' demand), hierarchical forecasting</td></tr>
<tr><td>5</td><td>Training horizon 674 days</td><td>Models may miss long-cycle patterns (multi-year trends)</td><td>More historical data + linear trend extrapolation</td></tr>
<tr><td>6</td><td>Lag-1 baseline slightly beats us on 10d WAPE</td><td>31.1% vs 33.8% — autocorrelation is strong</td><td>Incorporate lag-1 more directly; use it as fallback for very short horizons</td></tr>
<tr><td>7</td><td>No external data (weather, economic indicators)</td><td>Missing macro-level demand signals</td><td>Add economic calendar, weather data, promotion schedules</td></tr>
<tr><td>8</td><td>Single-warehouse model</td><td>Optimization doesn't account for inter-warehouse transfers</td><td>Multi-warehouse network optimization</td></tr>
</table>
</div>
<hr>
<div style="text-align:center; padding: 20px; color: #8b949e;">
<p><strong>MobAI'26 — AI-Powered Warehouse Management System</strong></p>
<p>Author: Aziba Mohammed Ayoub | GitHub: <a href="https://github.com/Azjob21/mobai-api" style="color:var(--accent)">Azjob21/mobai-api</a> | API: <a href="http://4.251.194.25:8000" style="color:var(--accent)">4.251.194.25:8000</a></p>
<p style="font-size: 0.85em;">Generated February 2026 — For personal reference</p>
</div>
</body>
</html>