Skip to content

Commit 9b956fa

Browse files
feat(experiments): modified v2 version of synthetic data
1 parent 6b84fcb commit 9b956fa

5 files changed

Lines changed: 137 additions & 14 deletions

File tree

-17.5 KB
Binary file not shown.
-156 Bytes
Binary file not shown.
-247 Bytes
Binary file not shown.
-103 Bytes
Binary file not shown.

scripts/generate_data.py

Lines changed: 137 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -175,23 +175,146 @@ def _experiment_synthetic_v1() -> None:
175175

176176

177177
def _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("\nholding_cost per destination:")
303+
for row in destinations.iter_rows(named=True):
304+
print(f" {row['destination_id']}: {row['holding_cost']}")
305+
print("\nDemand 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

197320
if __name__ == "__main__":

0 commit comments

Comments
 (0)