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:
- Parts that declare a predecessor which doesn't exist in the master at all
- The same part number appearing twice, once with the linkage populated and once without
- Relationships declared from only one side — A says its successor is B, but B doesn't mention A
- Circular supersessions, where the chain eventually points back at itself
- Branches, where two different parts both claim the same predecessor
- Blank, malformed, and free-text dates
- Lineage types spelled six different ways
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:
| Column | Purpose |
|---|---|
part_number | The part being described |
predecessor_part | What this part replaces |
successor_part | What replaces this part |
release_date | When the part entered production |
end_of_life_date | When the part exits production |
lineage_type | backwards_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_number | chain_id | chain_sequence | chain_length | lineage_type | plan_this_part |
|---|---|---|---|---|---|
| PN-1000 | SSC-PN-1000 | 1 | 4 | — | False |
| PN-1001 | SSC-PN-1000 | 2 | 4 | backwards_compatible | False |
| PN-1002 | SSC-PN-1000 | 3 | 4 | backwards_compatible | False |
| PN-1003 | SSC-PN-1000 | 4 | 4 | backwards_compatible | True |
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.