Consolidate Shipments
Answer how many trucks could we have saved by bundling? from a scenario holding a shipment history. Shipments that were already allowed to share a truck are packed onto the fewest transports that still fit — proven to be the fewest, per group — and what comes back is the same scenario with those transports in place of the shipments, so the history and the plan sit side by side as two scenarios you can compare.
When to use this¶
- You have a year of shipment history and want to know what better bundling would have been worth, before paying anyone to study it
- You want to re-run that question with different rules — a wider date window, a bigger vehicle, a transport allowed to mix modes — and compare the answers as scenarios rather than as numbers in two run reports
- Somebody has given you a consolidation number and you want one you can defend, with the rule set stated and the arithmetic proven rather than asserted
This node does not design routes, schedule departures, or price anything. It says how few transports the same freight fits on under the rules you gave it, and it never invents a cost: the saving it reports is in transports.
What you need¶
A scenario whose transport records are a shipment history — one row per movement that happened, rather than one rate per lane. That means three fields your optimizer scenarios may not carry, and all three are on the transport record:
| Field | What it is |
|---|---|
| Reference | the shipment or transport number. Two movements on one lane on one day are told apart by this and nothing else, so without it a scenario can hold only one of them |
| Date | when the movement happened. The window is measured over it, and the weekly trend is read off it |
| Load | the measure that fills the vehicle — gross weight, loading metres, cubic volume. Say which in the load unit |
quantity and load are the two measures a capacity dimension can be read
from, and they are the two a scenario carries: pallets and kilograms, or
kilograms and loading metres. A third would need a column a transport record
does not have.
A transport record also needs a product, and a shipment export usually has
none. Map Transport refuses a row without one — a movement moved
something — so if your export has no article column, add one upstream with a
single value like Freight. Everything on the lane then agrees about it, and
May Mix leaves it alone.
The chain this node sits at the end of¶
Two workflows, and the second one is the study. The first turns the export into a scenario; the second loads it, packs it, and persists the plan as a scenario of its own.
flowchart LR
n1["Import Data"] --> n2["Filter"]
n2 --> n3["Map Transport"]
n3 --> n4["Create Scenario"]
- Import Data — the shipment export, as it came out of the system.
- Filter — drop what cannot be consolidated at all. Sea and air legs, internal transfers between your own sites, shipments under an incoterm your planners would never bundle across. Dropping them here rather than inside the analysis is what makes the exclusion a rule a reader can see and change.
- Map Transport — name the columns. The shipping point is the source, the ship-to is the target, the transport type is the mode, the shipment number is the reference, the gross weight is the load. If the export has no article column, a Script before this step adds one:
def main(input_data):
for row in input_data:
row["product"] = "Freight"
return input_data
Then the study itself:
flowchart LR
n1["Load Scenario"] --> n2["Consolidate Shipments (into Scenario Data)"]
n2 --> n3["Create Scenario"]
Load Scenario reads the history back, Consolidate Shipments packs it, and Create Scenario persists the result as Consolidated Plan. You now have two scenarios over the same freight, and every comparison the product can draw between two scenarios works on them.
A canonical rule set, to start from and argue with. Nothing mixes; the bucket is the ISO week; a 24-tonne limit on the load; a window of 6 days; bundles of at least 2. On the reference export of 67,101 shipments — 46,829 of them after the filter — that saves 925 transports against the 763 a fill-in-original-order spreadsheet rule finds over the identical groups: the same freight, the same constraints, 21% more saving, because a spreadsheet fills bins in whatever order the file happened to be sorted in.
Nothing has a default, and that is deliberate¶
Every setting below is required. There is no default on any of them, and the node refuses to run until each one is answered — you will see a configuration finding against the node naming what is still unset.
The reason is that these are business choices, not properties of your data. What
a vehicle holds, how far apart two shipments may be dated, whether a truck may
carry two products — each has more than one defensible answer, and the wrong one
is silent. A declared capacity of 24000 would be read as the capacity by
every agent that ever configured this node, including the ones whose client runs
40-tonne trains. So the tool asks instead, and the result states back every
answer it was given.
May Mix is the one setting where choosing nothing is an answer: it means every shipment on a vehicle shares its whole lane, which is the strictest rule there is.
Settings¶
| Setting | Options | Default | What it decides |
|---|---|---|---|
| May Mix | none, product, transport mode | none — choose | Which parts of a lane one transport may blur. Leave it empty and every shipment on a vehicle shares product and mode. Allow transport mode and several part loads become one full one; allow product and a truck carries two. A transport that mixes a field names none of it — a truck carrying two products carries neither alone — so the plan's flow row leaves that column blank. Source and target are never on this list: a transport leaves one place for one other. |
| Capacity | one or more dimensions, each a measure, a limit and a unit | none — choose | What one vehicle holds. The measure is quantity or load — the two amounts a transport record carries. A bundle must fit every dimension at once, so weight and loading metres together will each bind on different lanes, which is the point of declaring both. The unit is not converted. It is your statement that the column and the limit are denominated the same way, and it is restated in the result so a reader can check it. One vehicle profile per run: comparing unlike vehicles is a question about their cost, and this node values nothing in money. |
| Bucket | none, day, ISO week, month | none — choose | The calendar bucket one transport may not cross. A week is the rule a weekly transport plan is published on, and it is what most consultancy studies state. It is also what keeps each sub-problem small enough to prove: without a bucket, a lane is every shipment that ever ran on it. |
| Window Days | a whole number, 0 or above | none — choose | How many days apart the earliest and latest shipment in one bundle may be dated. 0 keeps a bundle inside one day, and 6 is the widest span that fits inside a calendar week. This is a tolerance, where the bucket is a partition — you usually want both. |
| Min Bundle Size | a whole number, 2 or above | none — choose | The fewest shipments a bundle may hold. A shipment that joins no bundle passes through unchanged as its own transport, so raising this above 2 forbids pairs rather than forbidding singletons. |
| Report By | one or more lane columns | none — choose | The columns top lanes by consolidation gain is rolled up by. Source and target give you lanes; target alone gives you destinations. A column May Mix lets a transport differ in is refused here, because a transport that mixes a column cannot be filed under it. |
What comes back¶
The scenario again. Its flows are the plan — one row per transport, with
the measures summed, the lane it agreed on, the day it leaves and the lowest
shipment number it carries as its reference. Everything else the scenario held
travels through untouched, and so does every flow this node could not pack.
cost is carried through as the sum of what the bundled shipments were charged.
A history says what the movements that happened cost; nothing in it says what
one bundled truck would cost, so the node does not guess, and the saving it
reports is in transports.
The study rides on the scenario under _consolidation:
| Key | What it carries |
|---|---|
method |
every rule and parameter actually applied, including the ones you did not think you were choosing |
baseline |
the picture today: one transport per shipment, with average utilisation per dimension |
consolidated |
the plan: how many transports, how many of them are bundles, and utilisation again |
comparison |
the answer — transports saved, the top lanes by gain, and a week-by-week baseline against plan |
bundles |
one row per bundle: its shipments, its dates and how full it is in every dimension |
assignment |
one row per shipment, naming the transport it travels on and how many days it waited |
excluded |
what took no part, with counts, reasons and examples |
optimality |
how many groups are proven optimal, and the gap on any that are not |
Getting the assignment out is one Scenario to Table and a Save Data. Scenario to Table takes the scenario and returns rows; Save Data then writes them as a data source you can download or paste beside your original export.
flowchart LR
n1["Consolidate Shipments"] --> n2["Scenario to Table (into Scenario Data)"]
n2 --> n3["Save Data"]
def main(scenario_data):
"""The column a planner pastes beside the original export."""
return scenario_data["_consolidation"]["assignment"]
The same shape reads any other part of the study — bundles for the plan,
comparison["weekly"] for the trend, excluded["unusable"] for what took no
part.
What "proven optimal" means here¶
Bin packing has a cheap lower bound — the fewest transports a group could possibly need, whatever plan you write — and a good one-pass plan. Where the two agree, that plan is optimal and nothing has to be searched. Where they do not, the node searches the sizes in between, and either finds a smaller plan or proves that none exists.
That is a stronger claim than a time-limited solver's, and it is made per group
rather than for the file as a whole. On the reference export, all but one of
9,144 groups are proven from the bounds alone. Where a group is large enough
that the search runs out of its allowance, the group is reported in
optimality.groups_with_a_gap with the plan it has and the size it is known not
to beat — never quietly presented as the optimum.
The bucket is what makes that possible. The lower bound knows about capacity and nothing about dates, so on a lane holding a year of shipments it sits far below anything the window allows and the node has to report a gap it cannot close. Set the bucket to a week and each sub-problem is one lane for one week, where the bound is usually already the answer.
What takes no part, and why nothing is dropped¶
Rows the node cannot pack are counted and reported by reason, with examples, and they take no part in any number — but every one of them is still in the scenario that comes back. An analysis that deleted part of the scenario it was handed would be a destructive edit wearing the name of a study.
- no_reference — no shipment number. This is the ordinary case rather than dirt: a flow your optimizer wrote is a rate over the horizon, not a movement, and there is nothing about a rate to consolidate
- duplicate_reference — two rows claiming one number, which is either a double export or a split delivery and this node cannot tell which
- no_lane — a blank source or target, so there is nowhere to pack it
- unreadable_date — the window cannot be measured
- unreadable_capacity — a blank or non-numeric amount in a dimension you declared; never read as zero, which would put a free shipment in every truck
Separately, a shipment that already fills a vehicle on its own is passed through rather than excluded. It is one transport before and one transport after, so it appears in the baseline and in the plan alike and can never read as a saving. On the reference export that is 5,254 of 46,829 shipments.
Common mistakes¶
- Wiring a table into it. This node reads a scenario. Map the export with Map Transport, persist it, and load it back — the canvas will refuse the edge before you run anything.
- Leaving the product unmapped. Map Transport refuses a transport record with no product, so the mapping stops before this node ever runs. One constant column upstream is the whole fix.
- Loading a scenario your optimizer produced. Its flows are rates with no
date and no reference, so nothing packs and the whole file lands in
excluded.no_reference. Consolidation reads a history. - Putting the calendar week in Window Days. A week is a partition; a window
is a tolerance.
window_days: 6does not mean the same calendar week — it means within six days of each other, which spans two weeks half the time. Set Bucket to the week and keep the window for how long freight may wait. - Mixing units in a capacity dimension. Nothing converts. If half your rows are in kilograms and half in tonnes, fix that in the mapping; the node will happily add them and the plan will be wrong in a way the utilisation figures make obvious only if you look.
- Reading the saving without the exclusions. Transports saved is a number
about the rows that took part.
excluded.share_of_rowsis how much of the scenario that was.