How many containers will a deep-sea terminal handle next week — and what should it do about it?
A SARIMAX model forecasts weekly container volume from five years of history plus named, dated drivers. The forecast is monitored the way a production model has to be. And it then sizes a berth and crane plan, because a volume forecast is only worth something if a decision hangs off it.
Inspired by automated deep-sea container terminals in the Port of Rotterdam. All data is synthetic; not affiliated with or endorsed by any terminal operator.
Seasonality alone cannot explain a container terminal. These go in as exogenous regressors, and the app shows what the model learned from each:
| Driver | What it is | Learned effect |
|---|---|---|
| Chinese New Year | Asian factories shut for ~2 weeks; Rotterdam feels the gap ~5 weeks later, one sailing time downstream | −18% on a week's volume, tight interval, highly significant |
| Pre-CNY rush | Shippers pull cargo forward before the shutdown | +9%, significant |
| Red Sea rerouting | Since Dec 2023 carriers route Asia–Europe around the Cape of Good Hope. As of mid-2026 that is still the baseline in MSC's and CMA CGM's network design, with Maersk transiting Suez selectively | −6%, significant — and switchable as a scenario |
| A16 heavy-truck restriction | Since 1 July 2026 trucks over 45 t are barred from the J.F. Kennedy viaduct while the Van Brienenoord bridge is renewed; landside collection shifts to barge and rail | not estimable yet — two weeks of data, and the app says so instead of inventing a number |
That last row is deliberate. A model that reports a confident coefficient after a fortnight of data is lying; showing the interval instead is the point.
- Fit on log volume. Holidays, reroutings and storms act proportionally, not in absolute boxes. Logs make the model additive and every coefficient readable as a percentage.
- Fourier terms, not a seasonal ARIMA order. Weekly data has period 52;
a
(P,D,Q,52)term would estimate 52 lags from five years of data and overfit. Two sine/cosine pairs capture the annual shape with four parameters.
The rest is deliberately plain: ARIMA(1,1,1) errors with drift. d=1 was
chosen by testing — d=0 variants scored worse on AIC and produced
significant but wrong coefficients, which is the failure mode that actually
hurts.
Accuracy is measured by rolling-origin backtest: refit at each cut-off and score only weeks the model never saw.
- MAPE 4.2%, bias −0.3%, stable across a 4-week horizon.
- Storms and incidents are not model inputs — nobody forecasts a gale thirteen weeks out. They land in the residuals, where the outlier detector finds them: 10 weeks flagged, 8 of which already carry an operational note from the terminal's own log. That is the triage loop a production model needs: flag, explain, decide.
- Drift is measured with PSI and a two-sample KS test on the recent error distribution versus the reference period, implemented in numpy so the maths stays inspectable.
Take a week from the forecast and a CP-SAT model turns it into a berth and crane plan: which vessel berths where, with how many cranes, starting when.
- Variables:
x[v,b,k]vessel → berth with k cranes,s[v]start time. - Constraints: berth length and draft must fit the vessel · no overlap per berth · the shared crane pool is never oversubscribed at any moment (cumulative) · nothing starts before its ETA.
- Objective: minimise demurrage for waiting plus penalties for departing after the requested time.
- Handling time is
60 min berthing + moves / (cranes × productivity × η), where η falls away above four cranes because they interfere.
Benchmarked against handling vessels in arrival order — a real dispatcher's heuristic, which just cannot look ahead.
python -m venv .venv
# Windows: .venv\Scripts\activate macOS/Linux: source .venv/bin/activate
pip install -r requirements.txt
streamlit run app.pyOptional: the assistant needs a free Groq key.
Copy .streamlit/secrets.toml.example to .streamlit/secrets.toml. Without a
key everything else still runs.
python scripts/selftest.py # forecast recovery, monitoring, plan invariants
python scripts/ui_test.py # headless UI test
python scripts/forecast_check.py # learned vs. true effects, in detailapp.py Streamlit UI: forecast, monitoring, capacity plan
forecast/
data.py weekly history, anchored to published port magnitudes
drivers.py the dated real-world drivers and the exog matrix
model.py SARIMAX fit, forecast, backtest, contributions
monitoring.py residuals, rolling accuracy, outliers, PSI/KS drift
optimizer/
data.py turns a forecast week into vessel calls
model.py CP-SAT berth allocation + crane assignment
baseline.py arrival-order benchmark
kpis.py shared KPIs and the crane-usage profile
scripts/ selftest, UI test, and the probes used to calibrate
- Rolling re-forecast and re-optimisation as weeks close and ETAs update.
- Drift as a gate, not a chart — past threshold, the plan waits for a human.
- Terminal system integration — read volumes and calls from the operating system, write the accepted plan back, with a migration path that runs old and new data models side by side before cutting over.
- Driver library — add and retire exogenous drivers as the world changes, with the interval on a new driver shown honestly until it earns its place.
Educational portfolio project. The series is synthetic but anchored to published figures (Rotterdam handled 14.2 million TEU in 2025, +3.1%). Vessel names are real container-ship names used as flavour only. No real terminal, operator, schedule or customer data is involved.