Skip to content

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"]
  1. Import Data — the shipment export, as it came out of the system.
  2. 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.
  3. 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
4. Create Scenario — persist it. Call it Shipment History.

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: 6 does 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_rows is how much of the scenario that was.