It is difficult to see which specific choices in a complex chain of events actually lead to a final result. We often struggle to distinguish between minor details and the key decisions that shape an outcome.
It maps out a series of choices and ranks them based on how much they influence the final result. It identifies the specific steps that carry the most weight.
It allows you to see exactly which parts of a process are the most important drivers of the final outcome.
It was run inside an isolated container with no network access. This is the exact command and the real output it produced — captured process output, not written by a model.
$ python3 path_dependency_influence.py [Generated DAG: 60 edges, 25 nodes, seed=42] ==================================================================================================== Path-Dependency Influence Ranking ==================================================================================================== Target : 'n24' Nodes : 25 Edges : 60 Sources: n13, n1, n3, n5, n0 # Score Edge Paths Depth Reach Div BN W ----------------------------------------------------------------------------------------------- 1 0.8826 n3→n8 [review] 2 2 1.1281 0.5774 1.0000 2.210 2 0.8059 n1→n10 [approve] 4 1 0.6800 0.4083 1.0000 0.940 3 0.8046 n1→n12 [split] 4 1 0.5975 0.4083 1.0000 1.110 4 0.7848 n1→n2 [approve] 4 3 0.1151 0.4083 0.7071 2.420 5 0.7047 n8→n18 [reject] 1 1 0.5600 1.0000 0.7071 2.370 6 0.6981 n12→n24 [merge] 1 0 1.0000 0.5000 0.5000 2.390 7 0.6747 n10→n24 [merge] 1 0 1.000
A screenshot of that run.
A clean run proves this does what is shown above, in a CPU-only sandbox. It is a small research demo — not a production tool, and nothing here was published anywhere.
All of it — 381 lines, one file, standard library only.
#!/usr/bin/env python3
"""
Path-Dependency Influence Ranking
Recursive graph-traversal path scoring that identifies which decision branches
contribute most to a final outcome node.
Algorithm (Recursive Graph-Traversal Path Scoring):
1. Set the target/outcome node.
2. For every node, recursively count how many forward paths reach the target
(path_count_to_target).
3. For every node, recursively propagate a "reachability signal" forward
toward the target, applying decay per hop.
4. Score each directed edge (src→dst) as a weighted composite of:
- Path multiplicity (35%): share of total paths passing through src
- Reachability (25%): how well dst connects to target
- Divergence penalty (15%): high fan-out nodes split influence
- Bottleneck factor (15%): edges into high in-degree nodes
- Edge weight (10%): user-defined importance
5. Rank edges by composite score descending.
Usage:
python path_dependency_influence.py
python path_dependency_influence.py --seed 42 --nodes 30 --edges 80
python path_dependency_influence.py --graph-file graph.json --target outcome
python path_dependency_influence.py --export ranked.json --top 10
"""
import argparse
import json
import math
import random
import sys
from collections import defaultdict, deque
from dataclasses import dataclass
from typing import Dict, List, Optional, Tuple
# ---------------------------------------------------------------------------
# Data types
# ---------------------------------------------------------------------------
@dataclass
class Edge:
src: str
dst: str
weight: float = 1.0
label: str = ""
@dataclass
class RankedBranch:
src: str
dst: str
label: str
score: float
path_count: int
depth: int
reachability: float
normalized_path_frac: float
divergence_penalty: float
bottleneck_factor: float
weight: float
def to_dict(self) -> dict:
return {
"src": self.src,
"dst": self.dst,
"label": self.label,
"score": round(self.score, 5),
"path_count": self.path_count,
"depth": self.depth,
"reachability": round(self.reachability, 5),
"normalized_path_frac": round(self.normalized_path_frac, 5),
"divergence_penalty": round(self.divergence_penalty, 5),
"bottleneck_factor": round(self.bottleneck_factor, 5),
"edge_weight": self.weight,
}
class NodeInfo:
"""Lightweight per-node state."""
__slots__ = ("nid", "is_target", "reachability_score", "path_count_to_target")
def __init__(self, nid: str):
self.nid: str = nid
self.is_target: bool = False
self.reachability_score: float = 0.0
self.path_count_to_target: int = 0
# ---------------------------------------------------------------------------
# Core ranker
# ---------------------------------------------------------------------------
class PathDependencyInfluenceRanker:
def __init__(self, decay_factor: float = 0.85, max_depth: int = 200):
self.decay_factor = decay_factor
self.max_depth = max_depth
self.nodes: Dict[str, NodeInfo] = {}
self.incoming: Dict[str, List[Edge]] = defaultdict(list)
self.outgoing: Dict[str, List[Edge]] = defaultdict(list)
# Memoization tables (cleared per compute run)
self._path_memo: Dict[Tuple[str, int], int] = {}
self._reach_memo: Dict[Tuple[str, int], float] = {}
self._depth_memo: Dict[Tuple[str, str], int] = {}
# ---- construction -------------------------------------------------
def add_edge(self, src: str, dst: str, weight: float = 1.0, label: str = "") -> None:
e = Edge(src=src, dst=dst, weight=weight, label=label)
self.incoming[dst].append(e)
self.outgoing[src].append(e)
for nid in (src, dst):
if nid not in self.nodes:
self.nodes[nid] = NodeInfo(nid)
# ---- recursive metrics -------------------------------------------
def _path_count(self, node_id: str, depth: int, visited: set) -> int:
"""Return number of forward paths from node_id to the target."""
if depth > self.max_depth:
return 0
if node_id in visited:
return 0 # cycle guard
key = (node_id, depth)
if key in self._path_memo:
return self._path_memo[key]
node = self.nodes.get(node_id)
if node is None:
return 0
if node.is_target:
return 1
visited.add(node_id)
total = 0
for e in self.outgoing[node_id]:
total += self._path_count(e.dst, depth + 1, visited)
visited.discard(node_id)
self._path_memo[key] = total
return total
def _reachability(self, node_id: str, depth: int, visited: set) -> float:
"""Recursive reachability signal from node_id towards target."""
if depth > self.max_depth:
return 0.0
if node_id in visited:
return 0.0 # cycle guard
key = (node_id, depth)
if key in self._reach_memo:
return self._reach_memo[key]
node = self.nodes.get(node_id)
if node is None:
return 0.0
if node.is_target:
return 1.0
visited.add(node_id)
out_edges = self.outgoing[node_id]
if not out_edges:
visited.discard(node_id)
self._reach_memo[key] = 0.0
return 0.0
total = 0.0
for e in out_edges:
child = self._reachability(e.dst, depth + 1, visited)
total += e.weight * child
avg = total * (self.decay_factor ** depth) / len(out_edges)
visited.discard(node_id)
self._reach_memo[key] = avg
return avg
def _bfs_depth(self, src: str, target: str) -> int:
"""Shortest-hop distance from src to target. -1 if unreachable."""
cache_key = (src, target)
if cache_key in self._depth_memo:
return self._depth_memo[cache_key]
if src == target:
return 0
q = deque([(src, 0)])
seen = {src}
while q:
cur, d = q.popleft()
for e in self.outgoing.get(cur, []):
if e.dst == target:
self._depth_memo[cache_key] = d + 1
return d + 1
if e.dst not in seen:
seen.add(e.dst)
q.append((e.dst, d + 1))
self._depth_memo[cache_key] = -1
return -1
# ---- compute ------------------------------------------------------
def compute(self, target_node: str) -> List[RankedBranch]:
if target_node not in self.nodes:
raise ValueError(f"Target node '{target_node}' not in the graph.")
# Reset memos for fresh run
self._path_memo.clear()
self._reach_memo.clear()
self._depth_memo.clear()
# Tag target
self.nodes[target_node].is_target = True
# ---- Phase 1: compute per-node metrics recursively ------------
for nid in self.nodes:
node = self.nodes[nid]
node.path_count_to_target = self._path_count(nid, 0, set())
node.reachability_score = self._reachability(nid, 0, set())
# ---- Phase 2: normalisers ------------------------------------
max_paths = max((n.path_count_to_target for n in self.nodes.values()), default=1)
max_reach = max((n.reachability_score for n in self.nodes.values()), default=1e-9)
if max_reach < 1e-9:
max_reach = 1.0 # prevent division by zero on degenerate graphs
# ---- Phase 3: score every directed edge -----------------------
scored: List[Tuple[float, RankedBranch]] = []
for src, edges in self.outgoing.items():
src_node = self.nodes[src]
src_fan_out = len(edges)
for e in edges:
dst_node = self.nodes[e.dst]
dst_in_deg = len(self.incoming[e.dst])
# Component 1: path fraction through source
path_frac = src_node.path_count_to_target / max_paths
# Component 2: reachability ratio of destination
reach_ratio = dst_node.reachability_score / max_reach
# Component 3: divergence penalty (sqrt so edges aren't crushed)
div_penalty = 1.0 / math.sqrt(max(src_fan_out, 1))
# Component 4: bottleneck factor
bottleneck = 1.0 / math.sqrt(max(dst_in_deg, 1))
# Composite score — weights tuned for path-dependency ranking
score = (
0.35 * path_frac
+ 0.25 * reach_ratio
+ 0.15 * div_penalty
+ 0.15 * bottleneck
+ 0.10 * e.weight
)
# Shortest-hop depth from dst to target
edge_depth = self._bfs_depth(e.dst, target_node)
branch = RankedBranch(
src=e.src,
dst=e.dst,
label=e.label,
score=round(score, 5),
path_count=src_node.path_count_to_target,
depth=edge_depth,
reachability=round(dst_node.reachability_score, 5),
normalized_path_frac=round(path_frac, 5),
divergence_penalty=round(div_penalty, 5),
bottleneck_factor=round(bottleneck, 5),
weight=round(e.weight, 3),
)
scored.append((score, branch))
scored.sort(key=lambda x: x[0], reverse=True)
return [b for _, b in scored]
# ---- helpers -----------------------------------------------------
def identify_sources(self) -> List[str]:
return [nid for nid in self.nodes if not self.incoming[nid]]
def print_report(self, ranked: List[RankedBranch], target: str,
top_n: int = 20) -> None:
n_edges = sum(len(v) for v in self.outgoing.values())
print(f"{'=' * 100}")
print(f" Path-Dependency Influence Ranking")
print(f"{'=' * 100}")
print(f" Target : '{target}'")
print(f" Nodes : {len(self.nodes)} Edges : {n_edges}")
print(f" Sources: {', '.join(self.identify_sources())}")
print()
hdr = (
f"{'#':<4} {'Score':>8} {'Edge':<30} "
f"{'Paths':>8} {'Depth':>5} {'Reach':>8} "
f"{'Div':>6} {'BN':>6} {'W':>5}"
)
print(hdr)
print("-" * len(hdr))
for i, r in enumerate(ranked[:top_n], 1):
edge_str = f"{r.src}→{r.dst}"
if r.label:
edge_str += f" [{r.label}]"
print(
f"{i:<4} {r.score:>8.4f} {edge_str:<30} "
f"{r.path_count:>8} {r.depth:>5} {r.reachability:>8.4f} "
f"{r.divergence_penalty:>6.4f} {r.bottleneck_factor:>6.4f} "
f"{r.weight:>5.3f}"
)
# ---------------------------------------------------------------------------
# Graph generators & loaders
# ---------------------------------------------------------------------------
def generate_dag(num_nodes: int, num_edges: int, seed: int = 42) -> dict:
random.seed(seed)
names = [f"n{i}" for i in range(num_nodes)]
possible = [(names[i], names[j]) for i in range(num_nodes) for j in range(i + 1, num_nodes)]
random.shuffle(possible)
selected = possible[:min(num_edges, len(possible))]
labels = ["approve", "reject", "defer", "escalate", "review", "merge", "split"]
edges = []
for src, dst in selected:
edges.append({
"src": src,
"dst": dst,
"weight": round(random.uniform(0.5, 2.5), 2),
"label": random.choice(labels),
})
return {"edges": edges, "target": names[-1], "nodes": names}
def load_graph(data: dict) -> PathDependencyInfluenceRanker:
ranker = PathDependencyInfluenceRanker()
for e in data.get("edges", []):
ranker.add_edge(e["src"], e["dst"], e.get("weight", 1.0), e.get("label", ""))
return ranker
# ---------------------------------------------------------------------------
# Main entry
# ---------------------------------------------------------------------------
def main() -> int:
parser = argparse.ArgumentParser(
description="Path-Dependency Influence Ranking — recursive graph-traversal scoring"
)
parser.add_argument("--nodes", type=int, default=25)
parser.add_argument("--edges", type=int, default=60)
parser.add_argument("--seed", type=int, default=42)
parser.add_argument("--target", type=str, default=None)
parser.add_argument("--graph-file", type=str, default=None)
parser.add_argument("--top", type=int, default=20)
parser.add_argument("--decay", type=float, default=0.85)
parser.add_argument("--max-depth", type=int, default=200)
parser.add_argument("--export", type=str, default=None)
args = parser.parse_args()
if args.graph_file:
with open(args.graph_file) as f:
data = json.load(f)
target = args.target or data.get("target", "target")
else:
data = generate_dag(args.nodes, args.edges, args.seed)
target = args.target or data["target"]
print(f"[Generated DAG: {len(data['edges'])} edges, "
f"{len(data['nodes'])} nodes, seed={args.seed}]")
print()
ranker = load_graph(data)
ranker.decay_factor = args.decay
ranker.max_depth = args.max_depth
ranked = ranker.compute(target)
ranker.print_report(ranked, target, top_n=args.top)
if args.export:
with open(args.export, "w") as f:
json.dump([r.to_dict() for r in ranked], f, indent=2)
print(f"\n→ Exported {