What this establishes. A single, tagged, offline-reproducible multi-layer substrate — one node table and one edge table — merging six independent evidence layers already frozen in this repo: Raghunath et al. 2015 mechanistic melanogenesis network (T0, directed+signed), KEGG hsa04916 + Reactome curated union (T1, gene-membership only), GRN TF→target regulons from NB6 (T1, directed+signed), OmniPath literature validation from NB2 (T2b), our STRING v12.0 pull (T3, association), and D’Arcy et al. 2023 Table S4 STRING PPI (T3, association). D’Arcy’s OMIM disease annotation (Table S1), mass-spec proteome (Table S6), and the Bajpai et al. 2023 CRISPR screen hit list are folded in as node-level annotations only — by design they never create or delete an edge. This is the object NB8’s rescue screen runs against, so every node carries supporting_layers and every edge carries supporting_layers + layer_evidence_type + tier, so NB8 can say exactly which layer rescued each of the 52 unexplained loci.
Data pulled + counts. 803 nodes, 7819 unordered gene-pairs (edges) across 4 tiers: T0 Raghunath (mechanistic) = 293, T1 GRN regulon = 39, T2b OmniPath validation = 1, T3 STRING association = 7486. KEGG and Reactome contribute node membership only — neither frozen pull is an edge list (both are gene-link/participant endpoints), so no gene-gene edge was fabricated from either.
Contribution to the flagship. This is the substrate NB8 tests the 52 unresolved loci against. 1020 node-pairs (13.0%) have independent convergence from >=2 layers, and 138 pairs from >=3 layers — this convergence count is the signal NB8 will grade rescue confidence against.
The one number that matters. The STRING enzyme-class-token artifact (Raghunath’s PLC/PKC/PLA2/ MMPs/Trypsin/Phosphodiesterase placeholder nodes querying STRING as literal strings) was resolved against frozen HGNC gene-group membership: of 6 tokens, STRING fuzzy-matched 5 to a single gene each and left 1 unmapped; only 1 of those 5 (TRYPSIN→PRSS1) is a genuine HGNC group member, the other 4 (MMPs→BSG, PLC→HSPG2, PKC→PRRT2, Phosphodiesterase→SMPDL3A) are spurious fuzzy matches with no biological basis — 7 STRING edges were dropped rather than mis-attributed. The release-blocking citation gate PASSED: 8622 nodes+edges checked, 0 uncited.
Key terms — so this notebook stands on its own (you shouldn’t need the other notebooks to read this one).
The rescue screen (NB8) / “the flagship” — the project’s main analysis, which lives in a downstream notebook (NB8): it takes effector-uncertain pigmentation-association loci — where which gene actually drives the trait is genuinely open (not loci that already sit at a canonical gene like TYR or OCA2) — and tries to connect each into a known pigmentation network, grading confidence by how many independent evidence lines converge. This notebook builds the single merged network (“substrate”) that NB8 runs against; “the flagship” refers to that rescue screen.
The 52 unexplained (unresolved) loci — the 52 pigmentation-association loci (out of 105 hand-curated from published papers in NB3) whose authors reported a statistical association but could not say which gene or mechanism drives it. These are the targets NB8 tries to “rescue” by wiring them into the network.
Melanogenesis network / Raghunath T0 backbone — melanogenesis is the cellular pathway that makes melanin (pigment). The project’s mechanistic core is this pathway rebuilt as a directed, signed graph (each edge marked activation vs repression and A→B) from Raghunath et al. 2015; here it is the highest-trust evidence tier, T0.
GRN / regulon — GRN = gene-regulatory network: transcription-factor→target edges, signed (activation/repression). The “regulons” merged here are the sets of target genes controlled by the pigmentation transcription factors MITF/SOX10/PAX3, extracted in an upstream notebook (NB6).
STRING — a public database of functional protein–protein associations (evidence that two proteins are linked, e.g. co-expression, with no mechanism and no direction). Two STRING sources are folded in: a fresh v12.0 pull and the published STRING table from D’Arcy et al. 2023.
OmniPath / DoRothEA / CollecTRI — curated databases of signed, directed regulatory interactions (which TF activates or represses which target). OmniPath supplies the NB2 “validation” layer that checks Raghunath’s edges against the literature; DoRothEA/CollecTRI supply the GRN regulons.
Bajpai CRISPR screen — a CRISPR knockout screen (Bajpai et al. 2023) that flagged genes whose loss changes melanin production; its 169 “hit” genes are folded in as node annotations only — by design they never add or remove an edge.
Methods overview
Setup — resolve repo paths, assert every frozen input this notebook needs is on disk (Step 1).
Load all 6 evidence-layer sources from their already-frozen/processed files — no live network calls (Step 2).
Resolve the STRING enzyme-class-token artifact against the frozen HGNC gene-group pull before any STRING edge is attributed to a gene (Step 3).
Build the node universe from edge/association-bearing layers (Raghunath, KEGG, Reactome, GRN, STRING-ours, D’Arcy-STRING); fold in D’Arcy-OMIM / D’Arcy-mass-spec / Bajpai as node annotations only (Step 4).
Build the edge table: one row per unordered gene-pair, tagged with every supporting layer, a per-layer evidence type, a merged sign/direction only where a directed+signed layer (Raghunath or GRN) supports it, and the best (lowest-numbered) tier across all supporting layers (Step 5).
Node-pair convergence — count, per pair, how many independent layers support it (Step 6).
Citation-completeness gate — release-blocking check that every node and edge carries a resolvable citation (Step 7).
Figure + write outputs (Step 8).
Step 1 — Setup and frozen-input verification
Show code
import jsonfrom pathlib import Pathfrom collections import defaultdictimport pandas as pdimport numpy as npimport matplotlib.pyplot as pltROOT = Path.cwd().parent if Path.cwd().name =="notebooks"else Path.cwd()FROZEN = ROOT /"data"/"external"/"db_responses"PROC = ROOT /"data"/"processed"FIGDIR = Path("figures")FIGDIR.mkdir(exist_ok=True)REQUIRED_FROZEN_FILES = [ PROC /"gene_network_nodes.csv", PROC /"gene_network_edges.csv", FROZEN /"kegg_hsa04916.json", FROZEN /"reactome_pigmentation_curated_union.json", PROC /"nb6_grn_edges.csv", PROC /"darcy2023_S1_disease_genes.csv", PROC /"darcy2023_S6_massspec_expression.csv", PROC /"darcy2023_S4_string_edges.csv", PROC /"nb5_gene_set_membership.csv", FROZEN /"string_network_pulls_v12.json", PROC /"nb2_omnipath_validation.csv", FROZEN /"hgnc_gene_groups.json",]missing = [str(p) for p in REQUIRED_FROZEN_FILES ifnot p.exists()]assertnot missing, f"Missing required frozen inputs: {missing}"print("All", len(REQUIRED_FROZEN_FILES), "required frozen inputs present on disk.")
All 12 required frozen inputs present on disk.
Step 2 — Load all six evidence-layer sources
All inputs are already frozen/processed in-repo by prior notebooks (NB1/NB2/NB5/NB6). No live network calls are made here — this notebook is a pure offline merge of already-committed data.
Raghunath T0 — gene_network_nodes.csv (168 rows) + gene_network_edges.csv (309 rows): directed, signed mechanistic melanogenesis backbone (Raghunath et al. 2015, BMC Res Notes 8:170, DOI:10.1186/s13104-015-1128-6).
GRN regulon — nb6_grn_edges.csv (58 edges): directed, signed TF→target transcriptional regulation for MITF/SOX10/PAX3 (DoRothEA/CollecTRI, OmniPath-sourced).
D’Arcy annotation + association — darcy2023_S1_disease_genes.csv (OMIM, 278 rows/243 genes), darcy2023_S6_massspec_expression.csv (proteome, 4232 genes), darcy2023_S4_string_edges.csv (STRING PPI, 4668 edges/452 genes) — D’Arcy & Kiel 2023, Bioengineering 10(1):13, DOI:10.3390/bioengineering10010013, PMC9854651, CC BY 4.0.
Bajpai CRISPR annotation — flags embedded in nb5_gene_set_membership.csv (169 hit genes) — Bajpai et al. 2023, Science 381:eade6289, DOI:10.1126/science.ade6289, PMC10901463, CC BY 4.0.
OmniPath validation — nb2_omnipath_validation.csv (429 rows): per-edge literature-curated agree/disagree verdicts against the Raghunath backbone.
HGNC gene groups — hgnc_gene_groups.json: frozen HGNC group membership for the 6 enzyme-class tokens, used in Step 3 to resolve the STRING mapping artifact.
Step 3 — Resolve the STRING enzyme-class-token artifact
Six Raghunath nodes are not genes but enzyme-class placeholders (PLA2, MMPs, Trypsin, Phosphodiesterase, PKC, PLC) — each annotated in the Raghunath node table with an HGNC gene-group accession (e.g. HGNC:GG:467 for PLA2 = “Phospholipases”, 20 members). When our fresh STRING v12.0 pull queried these tokens as literal strings, STRING’s fuzzy symbol-matcher resolved most of them to a single, arbitrary gene with no relationship to the enzyme class — this is the artifact we must resolve before any STRING edge touching these tokens is attributed to a specific gene, or we would be silently asserting gene-gene edges for the wrong gene.
We check each fuzzy match against the frozen HGNC gene-group membership list:
Show code
class_nodes = rag_nodes[rag_nodes["node_class"] =="enzyme_activity_class"]token_to_hgnc_key = {"PLA2": "PLA2", "MMPs": "MMPs", "Trypsin": "Trypsin","Phosphodiesterase": "Phosphodiesterase", "PKC": "PKC", "PLC": "PLC"}CLASS_TOKENS_UPPER = {t.upper() for t in token_to_hgnc_key}string_union = string_pulls["results"]["union_all"]node_by_query = {n["query"]: n for n in string_union["nodes"]}unmapped_set =set(string_union["unmapped"])resolution_rows = []for token, hgnc_key in token_to_hgnc_key.items(): tok_upper = token.upper() grp = hgnc_groups[hgnc_key] members =set(grp["members"])if tok_upper in unmapped_set: status, mapped_gene, in_group ="unmapped_by_string", None, Falseelif tok_upper in node_by_query: mapped_gene = node_by_query[tok_upper]["name"] in_group = mapped_gene in members status ="resolved_genuine_member"if in_group else"resolved_spurious_fuzzy_match"else: status, mapped_gene, in_group ="not_in_query_result", None, False resolution_rows.append({"raghunath_token": token, "string_query_symbol": tok_upper,"hgnc_group_id": grp["hgnc_group_id"], "hgnc_group_name": grp["name"],"n_hgnc_members": grp["n"], "string_fuzzy_mapped_gene": mapped_gene,"is_genuine_hgnc_member": in_group, "resolution_status": status, })token_resolution = pd.DataFrame(resolution_rows)token_resolution
# Rule: attribute a STRING edge to a fuzzy-matched gene ONLY if it is a genuine HGNC group member# (the TRYPSIN -> PRSS1 case). Drop the edge for any token whose fuzzy match is spurious rather than# mis-attribute it to an unrelated gene.SPURIOUS_STRING_GENES =set( token_resolution.loc[token_resolution["resolution_status"] =="resolved_spurious_fuzzy_match","string_fuzzy_mapped_gene"])print("Spurious STRING fuzzy matches to drop (not genuine HGNC group members):", SPURIOUS_STRING_GENES)print("Genuine matches kept:", token_resolution.loc[token_resolution["resolution_status"] =="resolved_genuine_member", ["raghunath_token", "string_fuzzy_mapped_gene"]].values.tolist())print("Unmapped by STRING (no edge possible):", token_resolution.loc[token_resolution["resolution_status"] =="unmapped_by_string", "raghunath_token"].tolist())
Spurious STRING fuzzy matches to drop (not genuine HGNC group members): {'SMPDL3A', 'PRRT2', 'HSPG2', 'BSG'}
Genuine matches kept: [['Trypsin', 'PRSS1']]
Unmapped by STRING (no edge possible): ['PLA2']
Step 4 — Build the node universe
Design rule: annotation-only layers (D’Arcy-OMIM, D’Arcy-mass-spec, Bajpai CRISPR) never create or delete nodes or edges. They only annotate genes that are already present via an edge/association-bearing layer. This is a deliberate scope choice: D’Arcy’s mass-spec table (Table S6) is an undifferentiated melanoma-cell-line whole-proteome background of 4,232 genes with no pigmentation- pathway curation of its own — treating it as a node-creating layer would flood the substrate with proteomic background and destroy the meaning of “convergence.” It also directly serves the flagship: a gene that is only an OMIM disease entry or only a Bajpai CRISPR hit, with zero wiring into the mechanistic/association backbone, is precisely the kind of “not yet mechanistically explained” candidate NB8’s rescue screen exists to test.
The node universe is therefore the union of genes appearing in any of the six edge/association-bearing layers: Raghunath, KEGG, Reactome, GRN, our STRING pull (post-token-resolution), and D’Arcy’s STRING pull.
# Per-gene edge-bearing-layer membershipin_rag = {g: (g in rag_genes) for g in all_genes}in_kegg = {g: (g in kegg_genes) for g in all_genes}in_reactome = {g: (g in reactome_genes) for g in all_genes}in_grn = {g: (g in grn_genes) for g in all_genes}in_string_ours = {g: (g in our_string_genes) for g in all_genes}in_darcy_string = {g: (g in darcy_string_genes) for g in all_genes}# Annotation-only layers: aggregate per-gene (D'Arcy S1 has multi-disease genes -> groupby)darcy_omim_agg = darcy_s1.groupby("gene").apply(lambda d: pd.Series({"omim_disease_names": "|".join(sorted(set(d["disease_name"].astype(str)))),"omim_phenotype_class": "|".join(sorted(set(d["phenotype_class"].astype(str)))),}), include_groups=False).reset_index()darcy_omim_map = darcy_omim_agg.set_index("gene").to_dict("index")darcy_massspec_map = darcy_s6.set_index("gene")[["a375_lfq_average", "fm55_lfq"]].to_dict("index")bajpai_cols = ["bajpai_castle_effect", "bajpai_q_value", "bajpai_direction", "bajpai_orphan_hit"]bajpai_map = membership.set_index("gene")[bajpai_cols].to_dict("index")print(f"D'Arcy-OMIM annotated genes: {len(darcy_omim_map)} | "f"D'Arcy-mass-spec annotated genes: {len(darcy_massspec_map)} | "f"Bajpai membership-table entries: {len(bajpai_map)}")
RAGHUNATH_CITATION ="DOI:10.1186/s13104-015-1128-6 (Raghunath et al. 2015, BMC Res Notes 8:170)"KEGG_CITATION ="KEGG:hsa04916"REACTOME_ACCESSIONS ="|".join(c["stId"] for c in reactome["components"])DARCY_CITATION ="PMC9854651 (D'Arcy & Kiel 2023, Bioengineering 10(1):13, DOI:10.3390/bioengineering10010013)"STRING_CITATION =f"STRING v12.0 (query_utc {string_pulls['query_utc']})"rag_node_citation = rag_nodes.set_index("gene")["citation"].to_dict()rag_node_citation_source = rag_nodes.set_index("gene")["citation_source"].to_dict()grn_citation_by_gene = {}for _, r in grn.iterrows():for g in (r["source_TF"], r["target"]): grn_citation_by_gene.setdefault(g, r["citation"])def build_node_citation(row): g = row["gene"] cites, sources = [], [] layers = row["supporting_layers"].split("|") if row["supporting_layers"] else []if"Raghunath"in layers: c = rag_node_citation.get(g)if pd.notna(c): cites.append(c); sources.append(rag_node_citation_source.get(g, "accession"))if"KEGG"in layers: cites.append(KEGG_CITATION); sources.append("accession")if"Reactome"in layers: cites.append(REACTOME_ACCESSIONS); sources.append("accession")if"GRN"in layers: c = grn_citation_by_gene.get(g)if c: cites.append(c); sources.append("pmid_or_database")if"STRING_ours"in layers: cites.append(STRING_CITATION); sources.append("database")if"DArcy_STRING"in layers: cites.append(DARCY_CITATION); sources.append("pmcid") seen, dedup =set(), []for c in cites:if c notin seen: dedup.append(c); seen.add(c)return pd.Series({"citation": ";".join(dedup), "citation_source": "|".join(sorted(set(sources)))})cite_cols = nb7_nodes.apply(build_node_citation, axis=1)nb7_nodes = pd.concat([nb7_nodes, cite_cols], axis=1)n_uncited_nodes = (nb7_nodes["citation"].isna() | (nb7_nodes["citation"].str.strip() =="")).sum()print("Nodes with empty citation:", n_uncited_nodes)
Nodes with empty citation: 0
Step 5 — Build the edge table
Each unordered gene-pair accumulates evidence from every layer that asserts it. KEGG and Reactome contribute zero edges — both frozen pulls are gene-membership lists (a KEGG pathway gene-link endpoint and a Reactome participants endpoint), not edge lists; asserting all-pairs-within-pathway edges from a gene list would be exactly the kind of fabricated gene-gene edge this project’s rule forbids, so T1 is populated here only by the GRN regulon layer.
Sign/direction rule: a merged sign/direction is written only when a directed+signed layer (Raghunath or GRN) supports the pair. Association layers (STRING-ours, D’Arcy-STRING) and the validation layer (OmniPath) are never coerced into a sign or direction — they stay tagged as association/validation evidence. When two directed+signed layers disagree on direction or sign for the same pair, both are flagged (direction_conflict / sign_conflict) rather than silently resolved.
Tiering: each edge gets the best (lowest-numbered) tier across all its supporting layers — T0 Raghunath > T1 GRN regulon > T2b OmniPath validation > T3 STRING association.
Show code
edge_acc = defaultdict(lambda: {"layers": {}})def pair_key(a, b):returntuple(sorted([a, b]))for _, r in rag_edges.iterrows(): key = pair_key(r["source"], r["target"]) edge_acc[key]["layers"].setdefault("Raghunath", []).append( {"source": r["source"], "target": r["target"], "sign": r["sign"], "citation": r["citation"]})grn_sign_norm = {"activation": "+", "repression": "-", "unsigned": "unsigned", "ambiguous": "ambiguous"}for _, r in grn.iterrows(): key = pair_key(r["source_TF"], r["target"]) edge_acc[key]["layers"].setdefault("GRN", []).append( {"source": r["source_TF"], "target": r["target"],"sign": grn_sign_norm.get(r["sign"], r["sign"]), "citation": r["citation"]})omni_val_rows = omnipath_val[omnipath_val["verdict"].isin(["confirmed", "sign_conflict"])]for _, r in omni_val_rows.iterrows(): key = pair_key(r["src_sym"], r["tgt_sym"]) edge_acc[key]["layers"].setdefault("OmniPath_validation", []).append( {"source": r["src_sym"], "target": r["tgt_sym"], "citation": r["citation"]})# STRING-ours: resolve class tokens per Step 3; drop edges touching a spurious fuzzy matchname_to_query = {n["name"]: n["query"] for n in string_union["nodes"]}def resolve_string_name(query_symbol, name):if query_symbol in CLASS_TOKENS_UPPER:returnNoneif name in SPURIOUS_STRING_GENES else namereturn namen_string_dropped, n_string_kept =0, 0for e in string_union["edges"]: a_q = name_to_query.get(e["a"], e["a"]); b_q = name_to_query.get(e["b"], e["b"]) a_r = resolve_string_name(a_q, e["a"]); b_r = resolve_string_name(b_q, e["b"])if a_r isNoneor b_r isNone: n_string_dropped +=1continue key = pair_key(a_r, b_r) edge_acc[key]["layers"].setdefault("STRING_ours", []).append( {"source": a_r, "target": b_r, "score": e["score"], "citation": STRING_CITATION}) n_string_kept +=1for _, r in darcy_s4.iterrows(): key = pair_key(r["node1"], r["node2"]) edge_acc[key]["layers"].setdefault("DArcy_STRING", []).append( {"source": r["node1"], "target": r["node2"], "score": r["combined_score"], "citation": r["citation"]})print(f"STRING-ours: kept {n_string_kept} edges, dropped {n_string_dropped} for touching a spurious "f"class-token fuzzy match")print(f"Total unique unordered pairs across all edge-bearing layers: {len(edge_acc)}")
STRING-ours: kept 3830 edges, dropped 7 for touching a spurious class-token fuzzy match
Total unique unordered pairs across all edge-bearing layers: 7819
Show code
TIER_ORDER = {"T0_Raghunath": 0, "T1_GRN_regulon": 1, "T2b_OmniPath_validation": 2, "T3_STRING_association": 3}LAYER_TO_TIER = {"Raghunath": "T0_Raghunath", "GRN": "T1_GRN_regulon","OmniPath_validation": "T2b_OmniPath_validation","STRING_ours": "T3_STRING_association", "DArcy_STRING": "T3_STRING_association"}LAYER_EVIDENCE_TYPE = {"Raghunath": "mechanistic_signed_directed","GRN": "regulon_signed_directed_transcriptional","OmniPath_validation": "literature_curated_validation","STRING_ours": "association_unsigned_undirected","DArcy_STRING": "association_unsigned_undirected"}DIRECTED_SIGNED_LAYERS = {"Raghunath", "GRN"}edge_rows = []for key, acc in edge_acc.items(): layers_present =list(acc["layers"].keys()) tiers_present = {LAYER_TO_TIER[l] for l in layers_present} best_tier =min(tiers_present, key=lambda t: TIER_ORDER[t]) layer_evidence_types, citations, directed_signed_assertions = [], [], []for lname, records in acc["layers"].items(): layer_evidence_types.append(f"{lname}:{LAYER_EVIDENCE_TYPE[lname]}")for rec in records: c = rec.get("citation")if c and pd.notna(c): citations.append(str(c))if lname in DIRECTED_SIGNED_LAYERS:for rec in records: directed_signed_assertions.append((lname, rec["source"], rec["target"], rec.get("sign"))) merged_sign = merged_direction =None sign_conflict = direction_conflict =Falseif directed_signed_assertions: dirs =set((s, t) for (_, s, t, sg) in directed_signed_assertions) signs =set(sg for (_, s, t, sg) in directed_signed_assertions if sg notin (None, "unsigned", "ambiguous"))iflen(dirs) ==1: merged_direction =list(dirs)[0]else: direction_conflict =Trueiflen(signs) ==1: merged_sign =list(signs)[0]eliflen(signs) >1: sign_conflict =True merged_sign ="conflict"else: merged_sign ="unsigned" gene_a, gene_b = key edge_rows.append({"gene_a": gene_a, "gene_b": gene_b,"source": merged_direction[0] if merged_direction elseNone,"target": merged_direction[1] if merged_direction elseNone,"merged_sign": merged_sign, "sign_conflict": sign_conflict, "direction_conflict": direction_conflict,"supporting_layers": "|".join(sorted(layers_present)), "n_supporting_layers": len(layers_present),"layer_evidence_type": ";".join(layer_evidence_types), "tier": best_tier,"citation": ";".join(dict.fromkeys(citations)), })nb7_edges = pd.DataFrame(edge_rows)print(nb7_edges.shape)print(nb7_edges["tier"].value_counts().reindex(list(TIER_ORDER.keys())))print("sign_conflict:", nb7_edges["sign_conflict"].sum(), "| direction_conflict:", nb7_edges["direction_conflict"].sum())
For every node-pair (edge row), n_supporting_layers already counts how many of the 6 edge-bearing layers assert it. We tabulate the distribution and flag pairs with independent multi-layer convergence — the signal NB8’s rescue-confidence grading will use.
Show code
convergence_counts = nb7_edges["n_supporting_layers"].value_counts().sort_index()print("Node-pair convergence distribution (n layers -> n pairs):")print(convergence_counts)n_pairs_ge2 = (nb7_edges["n_supporting_layers"] >=2).sum()n_pairs_ge3 = (nb7_edges["n_supporting_layers"] >=3).sum()print(f"\nPairs with >=2 independent layers: {n_pairs_ge2} / {len(nb7_edges)}")print(f"Pairs with >=3 independent layers: {n_pairs_ge3} / {len(nb7_edges)}")top_conv = nb7_edges.sort_values("n_supporting_layers", ascending=False).head(10)top_conv[["gene_a", "gene_b", "supporting_layers", "n_supporting_layers", "tier"]]
Node-pair convergence distribution (n layers -> n pairs):
n_supporting_layers
1 6799
2 882
3 114
4 17
5 7
Name: count, dtype: int64
Pairs with >=2 independent layers: 1020 / 7819
Pairs with >=3 independent layers: 138 / 7819
Every node and every edge must carry >=1 resolvable citation (PMID/DOI/accession). Uses the citation-completeness-gate skill’s check_citation_completeness() helper.
A rough functional figure showing the tiered substrate: edge counts per evidence tier, and the node-pair convergence distribution across the 6 evidence layers.
<cell13_expr>:1: UserWarning: FigureCanvasAgg is non-interactive, and thus cannot be shown
Figure 1: The harmonized substrate stacks six evidence layers, most edges from STRING association. Edge counts per evidence tier (left) show mechanistic and regulon layers are small relative to the STRING layer; node-pair convergence (right) counts how many independent layers back each pair, with only 7 pairs supported by five layers.
print("=== NB7 FINAL SUMMARY ===")print(f"Total nodes: {len(nb7_nodes)}")print(f"Total edges (unordered pairs): {len(nb7_edges)}")print()print("Edges per tier:")print(nb7_edges["tier"].value_counts().reindex(tier_order_list, fill_value=0))print()print(f"Node-pairs with >=2 independent layers of support: {(nb7_edges['n_supporting_layers']>=2).sum()}")print(f"Node-pairs with >=3 independent layers of support: {(nb7_edges['n_supporting_layers']>=3).sum()}")print()print("STRING enzyme-class-token resolution:")print(token_resolution[["raghunath_token", "string_fuzzy_mapped_gene", "resolution_status"]].to_string(index=False))print(f" -> {n_string_kept} STRING edges kept, {n_string_dropped} dropped to avoid mis-attribution")print()print(f"Citation-completeness gate: PASSED -- {n_total} nodes+edges checked, {n_uncited} uncited")
=== NB7 FINAL SUMMARY ===
Total nodes: 803
Total edges (unordered pairs): 7819
Edges per tier:
tier
T0_Raghunath 293
T1_GRN_regulon 39
T2b_OmniPath_validation 1
T3_STRING_association 7486
Name: count, dtype: int64
Node-pairs with >=2 independent layers of support: 1020
Node-pairs with >=3 independent layers of support: 138
STRING enzyme-class-token resolution:
raghunath_token string_fuzzy_mapped_gene resolution_status
PLA2 None unmapped_by_string
MMPs BSG resolved_spurious_fuzzy_match
Trypsin PRSS1 resolved_genuine_member
Phosphodiesterase SMPDL3A resolved_spurious_fuzzy_match
PKC PRRT2 resolved_spurious_fuzzy_match
PLC HSPG2 resolved_spurious_fuzzy_match
-> 3830 STRING edges kept, 7 dropped to avoid mis-attribution
Citation-completeness gate: PASSED -- 8622 nodes+edges checked, 0 uncited
Known limitations and scope decisions
KEGG and Reactome are node-membership layers only in this substrate — neither frozen pull is an edge list, so asserting all-pairs-within-pathway edges from them would fabricate gene-gene relationships not supported by the source data. NB8 should treat KEGG/Reactome membership as a pathway-context tag on a node, not as edge evidence.
One STRING enzyme-class token (Trypsin) resolves to a genuine HGNC group member (PRSS1) and its STRING association edges are kept under PRSS1; the other 4 mapped tokens are spurious fuzzy matches and their STRING edges are dropped entirely (7 edges); PLA2 was unmapped by STRING and contributes no STRING edges. All 6 class-token placeholder nodes are retained in the node table (tagged is_raghunath_enzyme_class_token=True) purely as a record of the Raghunath T0 backbone’s own topology — they carry zero STRING-attributed edges.
9 directed-pair direction conflicts and 1 sign conflict were detected among directed+signed layers (mostly Raghunath’s own bidirectional regulatory loops, e.g. reciprocal MITF/PAX3 or NFKB1/TP53 edges in opposite directions with different signs — biologically plausible feedback, not a data error) and are flagged in the edge table (direction_conflict, sign_conflict) rather than silently collapsed.
This substrate does not yet include a signed/directed KEGG or Reactome reaction-level extraction; if a future notebook adds one, it should be added as its own edge-bearing layer with its own tier.