@@ -175,23 +175,146 @@ def _experiment_synthetic_v1() -> None:
175175
176176
177177def _experiment_synthetic_v2 () -> None :
178- """Generate experiments/datasets/synthetic_v2 — a larger scale dataset .
178+ """Generate experiments/datasets/synthetic_v2.
179179
180- 20 destinations across 10 origins with a full year of daily history (365 days).
181- Used to validate that the pipeline scales beyond the 6-destination toy example.
182- 10 origins (daily_capacity 80–180 each, ~1 300 total/day) comfortably covers
183- 20 destinations with base demands drawn from [20, 70] (~900 units/day mean).
180+ 365 days of daily demand (2024-01-01 → 2024-12-31) across 6 destinations
181+ and 3 origins. Each destination has a distinct demand pattern so that
182+ per-destination model selection has a genuine reason to choose different
183+ models:
184+
185+ - D1: strong weekly seasonality, level ~80, no trend, low noise
186+ - D2: flat, no seasonality, level ~60, low noise
187+ - D3: upward trend (+0.1/day), weak seasonality, level ~50, moderate noise
188+ - D4: strong weekly seasonality + upward trend, level ~90, moderate noise
189+ - D5: high noise, no pattern, level ~70
190+ - D6: weekly seasonality + mild downward trend, level ~100, low noise
191+
192+ Origins, destinations, lanes, and holding costs are all hand-assigned
193+ (no random generation) for full reproducibility and meaningful cost
194+ structure. Only demand noise uses numpy.random.seed(42).
184195 """
185- generate_synthetic_logistics_data (
186- output_dir = _PROJECT_ROOT / "experiments" / "datasets" / "synthetic_v2" ,
187- n_origins = 10 ,
188- n_destinations = 20 ,
189- start_date = datetime (2025 , 1 , 1 ),
190- end_date = datetime (2025 , 12 , 31 ),
191- seed = 7 ,
192- include_holding_cost = True ,
193- holding_cost_range = (0.5 , 2.0 ),
196+ import numpy as np
197+
198+ output_dir = _PROJECT_ROOT / "experiments" / "datasets" / "synthetic_v2"
199+ output_dir .mkdir (parents = True , exist_ok = True )
200+
201+ np .random .seed (42 )
202+
203+ start = datetime (2024 , 1 , 1 )
204+ dates = pl .date_range (
205+ start = start ,
206+ end = datetime (2024 , 12 , 30 ), # 2024 is a leap year; end on Dec 30 for exactly 365 days
207+ interval = "1d" ,
208+ eager = True ,
209+ )
210+ n_days = len (dates )
211+ day_of_week = [d .weekday () for d in dates ] # 0=Mon … 6=Sun
212+
213+ # --- origins ---
214+ origins = pl .DataFrame ({
215+ "origin_id" : ["O1" , "O2" , "O3" ],
216+ "daily_capacity" : [300.0 , 250.0 , 200.0 ],
217+ })
218+
219+ # --- destinations ---
220+ destinations = pl .DataFrame ({
221+ "destination_id" : ["D1" , "D2" , "D3" , "D4" , "D5" , "D6" ],
222+ "holding_cost" : [0.50 , 0.75 , 1.00 , 0.40 , 0.60 , 0.90 ],
223+ })
224+
225+ # --- lanes (hand-assigned, fully connected 3×6) ---
226+ lane_costs = {
227+ # O1: cheapest (2.0–4.0)
228+ ("O1" , "D1" ): 2.0 , ("O1" , "D2" ): 2.5 , ("O1" , "D3" ): 3.0 ,
229+ ("O1" , "D4" ): 2.2 , ("O1" , "D5" ): 3.5 , ("O1" , "D6" ): 4.0 ,
230+ # O2: mid-range (3.5–6.0)
231+ ("O2" , "D1" ): 3.5 , ("O2" , "D2" ): 4.0 , ("O2" , "D3" ): 4.5 ,
232+ ("O2" , "D4" ): 5.0 , ("O2" , "D5" ): 5.5 , ("O2" , "D6" ): 6.0 ,
233+ # O3: most expensive (5.0–8.0)
234+ ("O3" , "D1" ): 5.0 , ("O3" , "D2" ): 5.5 , ("O3" , "D3" ): 6.0 ,
235+ ("O3" , "D4" ): 6.5 , ("O3" , "D5" ): 7.0 , ("O3" , "D6" ): 8.0 ,
236+ }
237+ lanes = pl .DataFrame ([
238+ {"origin_id" : o , "destination_id" : d , "unit_cost" : c }
239+ for (o , d ), c in lane_costs .items ()
240+ ])
241+
242+ # --- demand history ---
243+ def seasonal_mult (pattern : list [float ]) -> np .ndarray :
244+ return np .array ([pattern [dow ] for dow in day_of_week ])
245+
246+ t = np .arange (n_days )
247+
248+ # D1: strong weekly seasonality, level ~80, no trend, low noise
249+ d1 = 80.0 * seasonal_mult ([1.20 , 1.15 , 1.00 , 0.90 , 0.85 , 0.70 , 0.75 ]) \
250+ + np .random .normal (0 , 4 , n_days )
251+
252+ # D2: flat, no seasonality, level ~60, low noise
253+ d2 = 60.0 + np .random .normal (0 , 3 , n_days )
254+
255+ # D3: upward trend, weak seasonality, level ~50, moderate noise
256+ d3 = (50.0 + 0.1 * t ) * seasonal_mult ([1.05 , 1.02 , 1.00 , 0.98 , 0.97 , 0.99 , 1.00 ]) \
257+ + np .random .normal (0 , 7 , n_days )
258+
259+ # D4: strong weekly seasonality + upward trend, level ~90, moderate noise
260+ d4 = (90.0 + 0.08 * t ) * seasonal_mult ([1.25 , 1.20 , 1.05 , 0.95 , 0.85 , 0.70 , 0.80 ]) \
261+ + np .random .normal (0 , 8 , n_days )
262+
263+ # D5: high noise, no pattern, level ~70
264+ d5 = 70.0 + np .random .normal (0 , 20 , n_days )
265+
266+ # D6: weekly seasonality + mild downward trend, level ~100, low noise
267+ d6 = (100.0 - 0.05 * t ) * seasonal_mult ([1.10 , 1.05 , 1.00 , 0.95 , 0.90 , 0.95 , 1.00 ]) \
268+ + np .random .normal (0 , 5 , n_days )
269+
270+ demand_rows = []
271+ for dest_id , raw in [("D1" , d1 ), ("D2" , d2 ), ("D3" , d3 ),
272+ ("D4" , d4 ), ("D5" , d5 ), ("D6" , d6 )]:
273+ clipped = np .clip (raw , 0 , None ).round (1 )
274+ for date , val in zip (dates , clipped ):
275+ demand_rows .append ({"date" : date , "destination_id" : dest_id , "demand" : float (val )})
276+
277+ demand_history = pl .DataFrame (demand_rows ).with_columns (
278+ pl .col ("date" ).cast (pl .Date )
279+ )
280+
281+ # --- validation ---
282+ assert len (demand_history ) == 2190 , f"Expected 2190 rows, got { len (demand_history )} "
283+ assert len (lanes ) == 18
284+ assert "holding_cost" in destinations .columns
285+ assert demand_history ["demand" ].min () >= 0.0
286+ for dest in ["D1" , "D2" , "D3" , "D4" , "D5" , "D6" ]:
287+ n = demand_history .filter (pl .col ("destination_id" ) == dest ).height
288+ assert n == 365 , f"{ dest } : expected 365 rows, got { n } "
289+
290+ # --- write ---
291+ origins .write_parquet (output_dir / "origins.parquet" )
292+ destinations .write_parquet (output_dir / "destinations.parquet" )
293+ lanes .write_parquet (output_dir / "lanes.parquet" )
294+ demand_history .write_parquet (output_dir / "demand_history.parquet" )
295+
296+ # --- summary ---
297+ print (f"synthetic_v2 written to { output_dir .resolve ()} " )
298+ print (f" origins.parquet : { len (origins )} rows" )
299+ print (f" destinations.parquet : { len (destinations )} rows" )
300+ print (f" lanes.parquet : { len (lanes )} rows" )
301+ print (f" demand_history.parquet : { len (demand_history )} rows" )
302+ print ("\n holding_cost per destination:" )
303+ for row in destinations .iter_rows (named = True ):
304+ print (f" { row ['destination_id' ]} : { row ['holding_cost' ]} " )
305+ print ("\n Demand statistics per destination:" )
306+ stats = (
307+ demand_history
308+ .group_by ("destination_id" )
309+ .agg ([
310+ pl .col ("demand" ).mean ().round (2 ).alias ("mean" ),
311+ pl .col ("demand" ).std ().round (2 ).alias ("std" ),
312+ pl .col ("demand" ).min ().round (2 ).alias ("min" ),
313+ pl .col ("demand" ).max ().round (2 ).alias ("max" ),
314+ ])
315+ .sort ("destination_id" )
194316 )
317+ print (stats )
195318
196319
197320if __name__ == "__main__" :
0 commit comments