Supply Chain Systems Program Leadership Cross-Functional Delivery
← Back to projects

Building a Supersession Chain Builder

How I used Python to turn a messy part master into clean, sequenced supersession chains — and cut manual lineage research out of the planning cycle.

The problem nobody sees until it costs money

Engineering releases a new part. To them, the work is done: the design is approved, the drawing is released, the part number exists.

To supply chain, the work has just started. Before anyone can plan that part, someone has to answer a deceptively simple question: what is this replacing?

That answer drives almost everything downstream. Demand history lives on the old part number, not the new one. Supplier commitments are still pointed at the old part. Safety stock, lead time assumptions, and forecast accuracy metrics are all attached to a part number that is about to stop selling. Get the linkage wrong and two things happen at once — the new part is under-forecast and goes short, while the old part keeps getting planned and quietly turns into obsolescence.

For years the answer came from planners doing manual research: opening the engineering change record, asking the design engineer, checking what the predecessor part used to sell, and stitching the relationship together by hand. Across thousands of active part numbers with new releases arriving continuously, that work never finished. It just got prioritized.

I built a tool to do it automatically.

What a supersession chain actually looks like

A single replacement is easy. What makes this hard is that parts don't get replaced once — they get replaced repeatedly, and the relationships form long chains across product generations:

PN-1000  →  PN-1001  →  PN-1002  →  PN-1003
(2018)      (2020)      (2022)      (2024)

A planner looking at PN-1003 needs to know it is the fourth generation of a lineage that started in 2018, that six years of demand signal sit upstream of it, and that three predecessors are sitting in inventory needing to be sold down rather than reordered.

The part master, however, does not store chains. It stores single hops — each row knows its immediate predecessor and successor, and nothing more. Turning thousands of individual hops into coherent, sequenced families is a graph problem, and that framing is what made the rest of the design fall out.

Not all supersessions are equal

Before writing any code I had to encode a rule that planners already knew intuitively but that lived nowhere in the data: the type of supersession changes the planning strategy entirely.

Fully interchangeable. The old and new parts substitute for each other in both directions. Demand can be netted across the whole chain and the family can be planned as a single item.

Backwards compatible. The new part can service the old application, but the old part cannot service the new one. Substitution is one-way, which means demand rolls forward to the newest part and predecessors get sold down — never reordered.

Not interchangeable. The parts share a design ancestry but cannot substitute for each other at all. Demand history does not roll forward, and both parts must be planned independently on their own use cases.

That third case is the one that quietly breaks naive implementations. It looks like a supersession in the source data, and it is one in the engineering sense — but treating it as a planning chain would roll history onto a part that will never inherit that demand. So the builder records the relationship and deliberately refuses to chain across it.

Designing for the data you actually get

The first version of this tool assumed clean input. That assumption survived about ten minutes of contact with production data. Real part masters contain:

The design decision that mattered most was this: never fail silently, and never fail loudly enough to stop. The builder handles every one of those cases deterministically and emits a diagnostics table alongside the output. Data quality problems become a report someone can work, instead of a wrong number nobody questions.

How the builder works

Five stages:

1. Reconcile the graph. Predecessor and successor columns are two views of the same relationship, and they disagree in practice. Both are read, converted into directed edges, and merged into one edge set. Where only one side declared the link, the evidence is accepted and flagged.

2. Clean the graph. Self-references and duplicates are dropped. Duplicate part rows are coalesced field by field rather than keeping the first row blindly, so a linkage populated only on the second row isn't thrown away. Circular supersessions are broken deterministically by treating the earliest-released part in the cycle as the origin.

3. Cut the non-interchangeable links. These edges stay in the output for audit, but they do not join two parts into one planning chain.

4. Assign a chain ID. Connected families are found with union-find, and each gets a stable identifier derived from its origin part. This is the piece that removed the manual work: nobody has to identify or maintain the groupings, because every part carries the ID of the family it belongs to.

5. Sequence and measure. Each part gets its generation position within the chain, and every part in the chain carries the chain's total length. A planner who wants to review every seven-generation lineage now writes one filter instead of doing a week of research.

Sample code:

uf = _UnionFind()
for node in nodes:
    uf.add(node)
for parent, child in retained_edges:
    uf.union(parent, child)

components = defaultdict(list)
for node in nodes:
    components[uf.find(node)].append(node)
def _generation_sequence(nodes, children, parents):
    """Longest-path depth from any chain origin. Origin parts get sequence 1."""
    indeg = {n: len(parents.get(n, [])) for n in nodes}
    seq = {n: 1 for n in nodes}
    stack = [n for n, d in indeg.items() if d == 0]
    while stack:
        node = stack.pop()
        for child in children.get(node, []):
            if seq[node] + 1 > seq[child]:
                seq[child] = seq[node] + 1
            indeg[child] -= 1
            if indeg[child] == 0:
                stack.append(child)
    return seq

Using longest-path depth rather than a simple walk matters for branched lineages: when two parts descend from the same predecessor, both are correctly labelled generation 2 rather than depending on which one the algorithm happened to visit first.

The whole thing runs in linear time over parts and relationships. On a synthetic master of ~225,000 parts it builds 108,000 chains in about fourteen seconds.

The input contract

Six columns, and only the first is mandatory:

ColumnPurpose
part_numberThe part being described
predecessor_partWhat this part replaces
successor_partWhat replaces this part
release_dateWhen the part entered production
end_of_life_dateWhen the part exits production
lineage_typebackwards_compatible, fully_interchangeable, or non_interchangeable

How to use the code:

from supersession_chain_builder import build_supersession_chains

result = build_supersession_chains(part_master)

result.parts        # every part, enriched with chain_id, sequence, length
result.chains       # one row per chain, with a planning recommendation
result.diagnostics  # every data-quality finding, severity-ranked
result.edges        # the reconciled graph actually used

# Every seven-generation lineage, in one filter
seven = result.chains[result.chains["chain_length"] == 7]

Sample output:

part_numberchain_idchain_sequencechain_lengthlineage_typeplan_this_part
PN-1000SSC-PN-100014False
PN-1001SSC-PN-100024backwards_compatibleFalse
PN-1002SSC-PN-100034backwards_compatibleFalse
PN-1003SSC-PN-100044backwards_compatibleTrue

plan_this_part is the column planners use most. It marks the live end of each chain — the part that should be carrying forecast — which means the answer to "what am I supposed to be buying?" is a filter rather than an investigation.

Impact

Manual research eliminated. Planners no longer trace predecessor and successor relationships by hand as new parts release. The lineage is derived from data that already exists.

Better forecast accuracy. With chains and generation sequence available, demand moves onto the right part number at the right time instead of lagging behind the engineering release. Suppliers get signal for the new part while it still matters.

Lower inventory cost and reduced obsolescence. This was the biggest win. Once the tool identifies the live end of every chain, forecast stops being applied to parts that are being superseded. Predecessors get sold down deliberately rather than reordered by inertia, and material spend stops accumulating against part numbers headed for end of life.

Scalability. The logic is generic. It doesn't care whether it's given a hundred parts or a million — there's no limit to how far the chain can grow.


Want to see it run? Try the live demo → — upload your own CSV or run it on sample data, entirely in your browser. The full module and sample test data are available on request. Built with Python and pandas.