The project’s primary finding. A third “the choice changes the answer” result — this time the choice is seeding.
TL;DR
What it establishes
142 of 169 CRISPR-validated melanin genes (Bajpai 2023) looked like “orphans” — in no curated pigmentation network — not because they are biologically disconnected, but because our STRING pull was seeded only on the curated gene sets. A STRING edge requires both endpoints in the query set, so an orphan↔︎curated edge was structurally impossible to observe. Re-querying STRING on the full 714-gene union (symmetric seed) reconciles 93 / 142 orphans to the melanogenesis core within ≤4 hops.
The third and most decisive “the choice changes the answer” result. NB5 showed network source drives 66%/34% membership drift; the tissue analyses showed melanocyte-vs-bulk flips locus calls; this shows query seeding flips the orphan count from 0 → 93. All three say the same thing: the substrate is a modeling choice, not a ground truth.
The one number that matters
0 → 93. Under the curated-only seed, 0 of 142 orphans can reach the core (they cannot even enter the graph). Under the symmetric seed, 93 reach it within ≤4 STRING hops.
Split
93 reconciled — connect to the core under the symmetric pull (network-completion result).
49 knowledge-gap residue — CRISPR-validated melanin genes with no ≤4-hop path to canonical machinery even under the symmetric pull. A finding in its own right: experimentally causal, mechanistically unwired. Listed in §4.
Guard rails (read before trusting any row)
STRING edges are association — undirected, unsigned. A STRING path is a hypothesis (association-tier), never a mechanism. We do not coerce sign or direction onto it.
The CRISPR hit is the causal anchor. Bajpai KO→reduced-melanin is the experimental evidence that the gene matters; STRING only supplies candidate wiring to explain how.
Bajpai sign is uniform — all 169 hits have positive casTLE effect (knockout reduces pigmentation). There is no sign contrast to model.
Convergence is thin and we report it as such. Of 93 reconciled, 92 are grade C (STRING path only); GRN and Reactome corroborate 0. 22/93 paths route through a promiscuous STRING hub (e.g. BRCA1, GRB2) and are flagged.
Key terms — so this notebook stands on its own (you shouldn’t need the other notebooks to read this one).
STRING — a public database of functional protein–protein associations (undirected, unsigned): an edge means two genes are functionally linked, not how or in which direction. Key quirk exploited here: STRING only returns an edge if both endpoints were in the gene list you queried. “Score ≥700” is a high-confidence edge cutoff.
Seeding (curated-only vs symmetric seed) — which genes you put INTO the STRING query. The original curated-only seed (464 curated genes) left the orphans out, so orphan↔︎core edges were structurally impossible to observe; the symmetric seed (the full 714-gene union) puts every orphan in the query, so those edges can appear. This one change flips the orphan count from 0 to 93 — the notebook’s finding.
Bajpai 2023 CRISPR screen / casTLE effect — a genome-wide CRISPR knockout screen (Bajpai et al. 2023) that flagged 169 genes whose knockout reduces melanin. casTLE is the screen’s effect-size statistic; all 169 hits have positive casTLE, so there is no sign contrast to model. These knockout hits are the experimentally causal anchor.
Orphan — a Bajpai CRISPR hit that appears in none of the curated pigmentation gene sets — a functionally validated melanin gene the curated maps missed (142 of the 169 hits).
Curated gene sets / core melanogenesis set — hand-collected pigmentation networks (Raghunath 2015, D’Arcy, KEGG hsa04916, Reactome pigmentation); their 464-gene union is the “curated” universe. The core is a 23-gene subset of canonical melanin-synthesis enzymes, melanosome proteins, and the MITF/SOX10/KIT machinery — the target the orphans must reach (within ≤4 STRING hops here).
GRN / regulon (from NB6) — gene-regulatory network: transcription-factor→target edges (here the MITF/SOX10/PAX3 regulons), built in notebook 6. Used as one independent evidence line for an orphan-to-core link.
Convergence grade (A/B/C) — how many independent evidence lines support a reconciled orphan’s connection (STRING path, direct core edge, GRN edge, Reactome-pigmentation tag): A ≥3, B = 2, C = 1. Most reconciled orphans here are grade C (STRING path only).
Hub-mediated path — a STRING path routed through a promiscuous, generic connector protein (BRCA1, GRB2, ACTB…) that links to almost everything; flagged because it inflates apparent reachability rather than reflecting a specific pigmentation link.
“The choice changes the answer” (NB5) — the project’s recurring point that a result depends on a modeling choice, not ground truth. Notebook 5 (NB5) showed the network source shifts gene membership ~66%/34%; this notebook shows query seeding flips the orphan count 0→93 — the third instance.
0 · Setup and frozen inputs
All inputs are frozen in-repo — no live network calls. Paths and checksums are verified before use.
Show code
import json, hashlibimport pandas as pd, numpy as np, networkx as nxfrom collections import Counterpd.set_option('display.max_colwidth', 70)import osfrom pathlib import PathREPO =str(Path.cwd().parent if Path.cwd().name =='notebooks'else Path.cwd())DBR =f'{REPO}/data/external/db_responses'PROC =f'{REPO}/data/processed'FROZEN = {'symmetric_pull' : f'{DBR}/string_union_symmetric_pull_v12.json', # NEW symmetric seed'curated_pull' : f'{DBR}/string_network_pulls_v12.json', # OLD curated-only seed'reactome_pig' : f'{DBR}/reactome_pigmentation_curated_union.json','bajpai' : f'{PROC}/bajpai2023_crispr_hits.csv','membership' : f'{PROC}/nb5_gene_set_membership.csv','grn_edges' : f'{PROC}/nb6_grn_edges.csv','substrate_edges' : f'{PROC}/nb7_substrate_edges.csv',}def sha8(p): h=hashlib.sha256()withopen(p,'rb') as f:for b initer(lambda:f.read(1<<16), b''): h.update(b)return h.hexdigest()[:8]import osfor k,p in FROZEN.items():assert os.path.exists(p), f'MISSING frozen input: {p}'print(f'{k:16} sha256={sha8(p)}{os.path.basename(p)}')
1 · The core melanogenesis set and the 142 orphans
Core melanogenesis set (23 genes, defined explicitly here — this is the target we ask orphans to reach): the canonical melanin-synthesis enzymes, melanosome structural/transport proteins, and the MITF/SOX10/EDN/KIT axis that builds and regulates the melanosome.
An orphan = a Bajpai CRISPR hit that is in none of the curated sets (Raghunath-168, D’Arcy-243, KEGG hsa04916, Reactome pigmentation) — i.e. in_bajpai2023_crispr_169 == True and n_sets == 0 in the NB5 membership table.
Show code
CORE = ['TYR','TYRP1','DCT','MLANA','PMEL','OCA2','HERC2','MC1R','KIT','KITLG','MITF','SLC45A2','SLC24A5','EDNRB','EDN3','POMC','ASIP','SOX10','PAX3','MLPH','RAB27A','MYO5A','GPR143']CORE =set(CORE)orphans = mem.loc[mem['in_bajpai2023_crispr_169'] & (mem['n_sets']==0), 'gene'].tolist()orphan_set =set(orphans)print('Core melanogenesis genes :', len(CORE))print('Bajpai CRISPR hits :', int(mem['in_bajpai2023_crispr_169'].sum()))print(' of which ORPHANS (n_sets==0):', len(orphans))assertlen(orphans)==142, len(orphans)# Bajpai sign is uniform — confirmprint('\nBajpai direction (all hits):', baj['direction_note'].str.contains('reduces').all(),'→ every KO reduces pigmentation (uniform positive-effector sign)')
Core melanogenesis genes : 23
Bajpai CRISPR hits : 169
of which ORPHANS (n_sets==0): 142
Bajpai direction (all hits): True → every KO reduces pigmentation (uniform positive-effector sign)
2 · The seeding artifact, demonstrated head-on
Why the orphans looked disconnected. STRING only reports a link between two genes if both of those genes are on the list of genes you ask it about. Our first query (saved as string_network_pulls_v12.json) asked only about the 464 genes in the curated networks. The 142 orphans were not on that list, so STRING could never report a link between an orphan and a curated gene — even if that link exists in reality. The orphans were not biologically “disconnected”; they were never asked about.
The fix. Ask STRING again, this time putting all 714 genes from every layer on the list (Raghunath + Bajpai + D’Arcy + KEGG + Reactome). Now every orphan is on the list, so a link between an orphan and a curated gene can be reported. This is the only thing that changed — same STRING version (v12.0), same score threshold (≥700).
Below we build both networks and count how many orphans reach the core under each gene list — the difference between the two counts is the result.
Show code
# STRING edges use the PREFERRED gene name, not the query symbol (e.g. KIAA1033 -> WASHC4).# Map query symbol -> preferred name so orphans/core align with edge endpoints.q2name = {n['query']: n['name'] for n in sym['nodes']}core_names = {q2name.get(c, c) for c in CORE}orphan_names = {o: q2name.get(o, o) for o in orphans}# --- Symmetric graph (name space) ---Gsym = nx.Graph()for e in sym['edges']: Gsym.add_edge(e['a'], e['b'], score=e['score'])for n in sym['nodes']: Gsym.add_node(n['name'])# --- Curated-only graph (union of the 4 curated component pulls) ---Gcur = nx.Graph()for k in ['raghunath_168','darcy_s1_243','kegg_hsa04916_101','reactome_r_hsa_5662702_5']:for e in old['results'][k]['edges']: Gcur.add_edge(e['a'], e['b'], score=e['score'])for n in old['results'][k]['nodes']: Gcur.add_node(n['name'])print(f'Symmetric graph : {Gsym.number_of_nodes():4d} nodes, {Gsym.number_of_edges():5d} edges')print(f'Curated-only graph: {Gcur.number_of_nodes():4d} nodes, {Gcur.number_of_edges():5d} edges')print(f'Core genes present — symmetric: {len(core_names &set(Gsym))}/23 curated-only: {len(core_names &set(Gcur))}/23')
def reaches_core(G, node, cores, cutoff):'''True if node reaches any core gene within `cutoff` hops.'''if node notin G: returnFalsefor c in cores:if c in G and nx.has_path(G, node, c) and nx.shortest_path_length(G, node, c) <= cutoff:returnTruereturnFalseCUTOFF =4# Under the CURATED-ONLY seed: orphans are not even nodes -> cannot reach coren_cur =sum(reaches_core(Gcur, orphan_names[o], core_names &set(Gcur), CUTOFF) for o in orphans)n_cur_nodes =sum((o in Gcur) or (orphan_names[o] in Gcur) for o in orphans)# Under the SYMMETRIC seedn_sym =sum(reaches_core(Gsym, orphan_names[o], core_names &set(Gsym), CUTOFF) for o in orphans)print('=== THE HEADLINE NUMBER ===')print(f'Orphans present as a node in curated-only graph : {n_cur_nodes:3d} / 142')print(f'Orphans reaching core (curated-only seed, <=4) : {n_cur:3d} / 142')print(f'Orphans reaching core (symmetric seed, <=4) : {n_sym:3d} / 142')print(f'\n 0 -> {n_sym} (seeding, not biology, produced the orphans)')
=== THE HEADLINE NUMBER ===
Orphans present as a node in curated-only graph : 0 / 142
Orphans reaching core (curated-only seed, <=4) : 0 / 142
Orphans reaching core (symmetric seed, <=4) : 93 / 142
0 -> 93 (seeding, not biology, produced the orphans)
3 · Per-orphan reconciliation: distance, path, and independent lines of evidence
For each of the 142 orphans we compute, in the symmetric graph:
Path intermediates and the weakest edge score on that path (the path is only as strong as its weakest link; a low weakest-score is a fragile hypothesis).
GRN-regulon membership — is it a MITF/SOX10/PAX3 target (NB6, nb6_grn_edges.csv)?
Reactome pigmentation-tagged — is it in the curated Reactome pigmentation union?
Direct core adjacency — distance == 1.
Convergence grade (reconciled orphans only) = number of independent lines supporting the connection (STRING path within cutoff, direct core edge, GRN edge, Reactome-pig tag): A ≥3 lines · B = 2 lines · C = 1 line. A path routed through a promiscuous STRING hub (BRCA1, GRB2, ACTB, …) is flagged hub_mediated_path — these are generic connectors and inflate reachability.
Show code
def nearest_core(G, src, cores): best=Nonefor c in cores:if c in G and nx.has_path(G, src, c): d=nx.shortest_path_length(G, src, c)if best isNoneor d<best[0]: best=(d,c)return bestdef path_details(G, s, t): p=nx.shortest_path(G, s, t) sc=[G[p[i]][p[i+1]]['score'] for i inrange(len(p)-1)]return p, p[1:-1], (min(sc) if sc else np.nan)react_pig =set(react['curated_union_genes'])grn_targets =set(grn['target'])HUBS = {'BRCA1','GRB2','ACTB','CUL5','APP','UBC','ELAVL1','MYC','EGFR','HSP90AA1','TP53','ESR1','EP300','CREBBP'}core_in_sym = core_names &set(Gsym)baj_idx = baj.set_index('Symbol')BAJPAI_CIT='Bajpai et al. 2023 Science 381:eade6289 (DOI 10.1126/science.ade6289; PMC10901463)'STRING_CIT='STRING v12.0 (Szklarczyk 2023, PMID 36370105); symmetric union pull score>=700, string_union_symmetric_pull_v12.json'rows=[]for o in orphans: nm=orphan_names[o]; in_sym = nm in Gsym nc = nearest_core(Gsym, nm, core_in_sym) if in_sym elseNoneif nc: dist,cg = nc; p,inter,weak = path_details(Gsym, nm, cg) inter_str =' | '.join(inter) if inter else'(direct)'; path_str=' -> '.join(p)else: dist=np.nan; cg=''; inter_str=''; path_str=''; weak=np.nan reached = (not np.isnan(dist)) and dist<=CUTOFF direct = dist==1 in_grn = o in grn_targets react_t = o in react_pig n_lines =int(reached)+int(bool(direct))+int(in_grn)+int(react_t)if reached: grade ='A'if n_lines>=3else ('B'if n_lines==2else'C'); bucket='reconciled'else: grade=''; bucket='knowledge-gap' hub =bool(inter_str) and inter_str!='(direct)'andany(x.strip() in HUBS for x in inter_str.split('|')) castle =float(baj_idx.loc[o,'Combined_casTLE_Effect']) if o in baj_idx.index else np.nan cit = BAJPAI_CIT + (' | '+STRING_CIT if bucket=='reconciled'else'') rows.append(dict(gene=o, casTLE=castle, in_symmetric_graph=in_sym, dist_to_core=dist, nearest_core_gene=cg, path_intermediates=inter_str, path_full=path_str, weakest_edge_score=weak, in_GRN=in_grn, reactome_pig_tagged=react_t, direct_core_adjacency=bool(direct), hub_mediated_path=hub, n_convergence_lines=n_lines, convergence_grade=grade, bucket=bucket, citation=cit))recon_df = pd.DataFrame(rows).sort_values(['bucket','dist_to_core','gene'], na_position='last').reset_index(drop=True)print('reconciled :', int((recon_df.bucket=="reconciled").sum()))print('knowledge-gap:', int((recon_df.bucket=="knowledge-gap").sum()))recon_df.head(6)[['gene','casTLE','dist_to_core','nearest_core_gene','path_full','weakest_edge_score','convergence_grade','bucket']]
reconciled : 93
knowledge-gap: 49
gene
casTLE
dist_to_core
nearest_core_gene
path_full
weakest_edge_score
convergence_grade
bucket
0
TMED2
1.3
5.0
POMC
TMED2 -> TMED10 -> PSENEN -> TNF -> CALM3 -> POMC
0.82
knowledge-gap
1
AC002398.9
1.7
NaN
NaN
knowledge-gap
2
ALKBH8
0.8
NaN
NaN
knowledge-gap
3
B4GALT1
1.5
NaN
NaN
knowledge-gap
4
BACE2
1.1
NaN
NaN
knowledge-gap
5
BLOC1S5-TXNDC5
2.3
NaN
NaN
knowledge-gap
Show code
# Distance distribution + grade distribution (reconciled)rec = recon_df[recon_df.bucket=='reconciled']print('Distance-to-core distribution (reconciled):')for d,c in rec['dist_to_core'].astype(int).value_counts().sort_index().items():print(f' {d} hop(s): {c}')print('\nConvergence grade distribution (reconciled):', rec['convergence_grade'].value_counts().to_dict())print(f'Paths through a promiscuous STRING hub: {int(rec.hub_mediated_path.sum())}/{len(rec)}')print(f'\nIndependent corroboration beyond STRING:')print(f' GRN-regulon targets among orphans : {int(recon_df.in_GRN.sum())}')print(f' Reactome-pig-tagged among orphans : {int(recon_df.reactome_pig_tagged.sum())}')print(f' direct core adjacency (dist==1) : {int(recon_df.direct_core_adjacency.sum())}')print('\n→ Reconciliation rests almost entirely on STRING association. This is the principal weakness.')
Distance-to-core distribution (reconciled):
1 hop(s): 1
2 hop(s): 38
3 hop(s): 35
4 hop(s): 19
Convergence grade distribution (reconciled): {'C': 92, 'B': 1}
Paths through a promiscuous STRING hub: 22/93
Independent corroboration beyond STRING:
GRN-regulon targets among orphans : 0
Reactome-pig-tagged among orphans : 0
direct core adjacency (dist==1) : 1
→ Reconciliation rests almost entirely on STRING association. This is the principal weakness.
4 · Split: reconciled vs knowledge-gap residue
(a) Reconciled (93) — connect to the core within ≤4 STRING hops. The network-completion result: genes that looked orphaned only because of the seeding artifact. STRING supplies a candidate wiring hypothesis; the CRISPR hit is the causal anchor.
(b) Knowledge-gap residue (49) — CRISPR-validated melanin genes with no ≤4-hop path to the core even under the symmetric pull. These are not artifacts of seeding: they are experimentally causal (KO reduces melanin) yet have no known high-confidence interaction wiring them to the canonical machinery. This is a genuine knowledge gap and a finding in its own right — the complement to the reconciliation result.
Show code
gap = recon_df[recon_df.bucket=='knowledge-gap'].copy()# split the residue by *why* it stays orphannot_in_graph = gap[~gap.in_symmetric_graph]in_graph_far = gap[gap.in_symmetric_graph]print(f'Knowledge-gap residue: {len(gap)} orphans')print(f' not mapped into STRING at all : {len(not_in_graph)}{sorted(not_in_graph.gene)}')print(f' in graph but no <=4-hop core path : {len(in_graph_far)}')print('\nFull knowledge-gap gene list (CRISPR-validated, mechanistically unwired):')print(sorted(gap.gene.tolist()))
Knowledge-gap residue: 49 orphans
not mapped into STRING at all : 3 ['AC002398.9', 'BLOC1S5-TXNDC5', 'C10ORF11']
in graph but no <=4-hop core path : 46
Full knowledge-gap gene list (CRISPR-validated, mechanistically unwired):
['AC002398.9', 'ALKBH8', 'B4GALT1', 'BACE2', 'BLOC1S5-TXNDC5', 'C10ORF11', 'C16ORF80', 'C9ORF91', 'CD72', 'CLN3', 'CMTR2', 'CTDSPL2', 'DGKD', 'F8A1', 'F8A3', 'FRYL', 'GOLGA8M', 'HAUS3', 'HSPA13', 'INTS6', 'INTS8', 'LZIC', 'MSMO1', 'MTMR6', 'MTMR9', 'RAB14', 'RPRD2', 'SCAF4', 'SELT', 'SHISA4', 'SLAIN1', 'SLC12A9', 'SLC26A6', 'SLC33A1', 'SNX13', 'SPAG16', 'TMED2', 'TMEM163', 'TMEM30A', 'TMEM9B', 'TRAPPC13', 'TRNAU1AP', 'VPS54', 'WDFY1', 'WDR81', 'WDR91', 'ZBTB1', 'ZC3H4', 'ZNF428']
Show code
# The reconciled set, strongest first (nearest, highest weakest-edge, non-hub)best = recon_df[recon_df.bucket=='reconciled'].copy()best = best.sort_values(['hub_mediated_path','dist_to_core','weakest_edge_score'], ascending=[True,True,False])best[['gene','casTLE','dist_to_core','nearest_core_gene','path_full','weakest_edge_score','convergence_grade','hub_mediated_path']].head(12)
gene
casTLE
dist_to_core
nearest_core_gene
path_full
weakest_edge_score
convergence_grade
hub_mediated_path
49
SLC24A4
0.7
1.0
MC1R
SLC24A4 -> MC1R
0.731
B
False
84
SLC31A1
2.5
2.0
POMC
SLC31A1 -> ATP7A -> POMC
0.938
C
False
66
GLTSCR1
1.0
2.0
SOX10
BICRA -> SMARCA4 -> SOX10
0.937
C
False
80
RB1
0.9
2.0
SOX10
RB1 -> SMARCA4 -> SOX10
0.937
C
False
68
HDAC3
1.2
2.0
POMC
HDAC3 -> STAT3 -> POMC
0.852
C
False
76
NCOR1
1.1
2.0
SOX10
NCOR1 -> SMARCA4 -> SOX10
0.820
C
False
60
CHD8
1.0
2.0
MITF
CHD8 -> CTNNB1 -> MITF
0.813
C
False
82
SIAH1
1.3
2.0
MITF
SIAH1 -> CTNNB1 -> MITF
0.813
C
False
85
TBL1XR1
1.1
2.0
MITF
TBL1XR1 -> CTNNB1 -> MITF
0.813
C
False
64
DEK
1.8
2.0
KIT
DEK -> TFAP2A -> KIT
0.802
C
False
61
COMMD3-BMI1
1.6
2.0
MC1R
COMMD3-BMI1 -> CDKN2A -> MC1R
0.793
C
False
50
ACTR2
1.4
2.0
MC1R
ACTR2 -> CDKN2A -> MC1R
0.766
C
False
5 · Confirming experiments for exemplar reconciled orphans
A STRING path is an association hypothesis. To promote it to mechanism, test the predicted physical interaction and the epistasis with the core partner. The confirming_experiment column carries a per-orphan proposal; a few exemplars:
Show code
exemplars = ['SLC24A4','SLC31A1','KIRREL','HDAC3','BLOC1S2']def confirming_exp(row):if row.bucket!='reconciled':return ('No canonical route under symmetric pull. Re-test phenotype (independent sgRNAs + melanin ''quantification), then screen for a physical partner de novo (BioID/proximity labeling) ''before wiring to the core.') core=row.nearest_core_geneif row.dist_to_core==1:return (f'Direct STRING edge to {core}. Co-IP / proximity-labeling (BioID) of {row.gene} vs {core}; 'f'epistasis via double-KO ({row.gene} x {core}) + melanin readout in melanocytes.') via=row.path_intermediates.replace(' | ',', ').split(', ')[0]return (f'STRING path to {core} via {row.path_intermediates.replace(" | ", ", ")}. Test predicted 'f'interaction {row.gene}-{via} (co-IP/BioID); epistasis double-KO ({row.gene} x {core}) + melanin readout.')recon_df['confirming_experiment'] = recon_df.apply(confirming_exp, axis=1)for g in exemplars: r=recon_df[recon_df.gene==g].iloc[0]print(f'• {g} (casTLE={r.casTLE}, dist={int(r.dist_to_core)} to {r.nearest_core_gene}, grade {r.convergence_grade}):')print(f' {r.confirming_experiment}\n')
• SLC24A4 (casTLE=0.7, dist=1 to MC1R, grade B):
Direct STRING edge to MC1R. Co-IP / proximity-labeling (BioID) of SLC24A4 vs MC1R; epistasis via double-KO (SLC24A4 x MC1R) + melanin readout in melanocytes.
• SLC31A1 (casTLE=2.5, dist=2 to POMC, grade C):
STRING path to POMC via ATP7A. Test predicted interaction SLC31A1-ATP7A (co-IP/BioID); epistasis double-KO (SLC31A1 x POMC) + melanin readout.
• KIRREL (casTLE=0.9, dist=2 to KIT, grade C):
STRING path to KIT via GRB2. Test predicted interaction KIRREL-GRB2 (co-IP/BioID); epistasis double-KO (KIRREL x KIT) + melanin readout.
• HDAC3 (casTLE=1.2, dist=2 to POMC, grade C):
STRING path to POMC via STAT3. Test predicted interaction HDAC3-STAT3 (co-IP/BioID); epistasis double-KO (HDAC3 x POMC) + melanin readout.
• BLOC1S2 (casTLE=1.0, dist=2 to HERC2, grade C):
STRING path to HERC2 via BRCA1. Test predicted interaction BLOC1S2-BRCA1 (co-IP/BioID); epistasis double-KO (BLOC1S2 x HERC2) + melanin readout.
6 · QC gates, figure, and output
Citation-completeness gate — every row must carry a resolvable source (the Bajpai CRISPR causal anchor for all; STRING v12 for reconciled rows). US spelling throughout. Frozen inputs verified in §0.
Citation-completeness gate: PASS (142/142 rows cited)
Wrote data/processed/nb9_orphan_reconciliation.csv (142, 17)
Show code
import matplotlib.pyplot as plt, matplotlib as mpl# Figure: (a) before/after 0->93; (b) distance-to-core histogram by convergence gradeplt.rcParams.update({'font.size':8,'axes.spines.top':False,'axes.spines.right':False,'axes.titlesize':8.5,'figure.dpi':110})META_GREY='#9a9a9a'rec = recon_df[recon_df.bucket=='reconciled']fig,(ax1,ax2)=plt.subplots(1,2,figsize=(9.2,4.0),gridspec_kw={'width_ratios':[1,1.35]})vals=[0,len(rec)]; bars=ax1.bar(['Curated-only\nseed (464 genes)','Symmetric\nseed (714 genes)'],vals, color=[META_GREY,'#2b7a9e'],width=0.62,zorder=3)ax1.set_ylabel('Orphans reaching melanogenesis core\n(<=4 STRING hops)'); ax1.set_ylim(0,100); ax1.margins(x=0.15)for b,v inzip(bars,vals): ax1.text(b.get_x()+b.get_width()/2,v+2,str(v),ha='center',va='bottom',fontweight='bold',fontsize=9)ax1.plot([bars[0].get_x(),bars[0].get_x()+bars[0].get_width()],[0.4,0.4],color=META_GREY,lw=3,zorder=4)ax1.set_title('Seeding controls reconciliation:\n0 -> %d of 142 orphans'%len(rec),loc='left')ax1.text(-0.16,1.05,'a',transform=ax1.transAxes,fontweight='bold',fontsize=12)xs=sorted(rec['dist_to_core'].astype(int).unique())c=[int(((rec.dist_to_core==d)&(rec.convergence_grade=='C')).sum()) for d in xs]b=[int(((rec.dist_to_core==d)&(rec.convergence_grade=='B')).sum()) for d in xs]ax2.bar(xs,c,width=0.7,color='#2b7a9e',label='grade C (STRING path only)',zorder=3)ax2.bar(xs,b,bottom=c,width=0.7,color='#d1892b',label='grade B (path + direct core edge)',zorder=3)ax2.set_xlabel('Shortest-path distance to nearest core gene (STRING hops)')ax2.set_ylabel('Reconciled orphans'); ax2.set_xticks(xs); ax2.set_ylim(0,max(ci+bi for ci,bi inzip(c,b))+6)for d,ci,bi inzip(xs,c,b): ax2.text(d,ci+bi+0.8,str(ci+bi),ha='center',va='bottom',fontsize=8)ax2.legend(frameon=False,fontsize=6.5,loc='upper right')ax2.set_title('Most reconciliation is single-line STRING association (%d C, %d B)'%( (rec.convergence_grade=='C').sum(),(rec.convergence_grade=='B').sum()),loc='left')ax2.text(-0.13,1.05,'b',transform=ax2.transAxes,fontweight='bold',fontsize=12)fig.suptitle('Bajpai CRISPR orphans reconcile to the melanogenesis core under a symmetric STRING pull', fontsize=10,fontweight='bold',x=0.02,ha='left',y=1.02)fig.tight_layout(); fig.savefig(f'{REPO}/notebooks/figures/nb9_orphan_reconciliation.png',dpi=300,bbox_inches='tight')print('saved figure'); fig
saved figure
Figure 1: A symmetric STRING seed reconciles 93 of 142 Bajpai CRISPR orphans to the melanogenesis core; a curated-only seed reconciles none. The seeding choice controls the outcome (left); reconciled orphans are almost entirely single-line STRING associations (grade C), by shortest-path distance to the nearest core gene (right).
7 · What this establishes (and what it does not)
Establishes. The orphan status of 142 CRISPR-validated melanin genes was a seeding artifact, not biology. Asking STRING the symmetric question (seed on the full union, so both endpoints of every candidate edge are in the query) reconciles 93/142 to the melanogenesis core within ≤4 hops — up from a structural 0 under the curated-only seed. This is the project’s third “the choice changes the answer” result: source (NB5), tissue (melanocyte-vs-bulk), and now seeding.
Does not establish. These are association hypotheses, not mechanisms. 92 of 93 reconciled orphans rest on a single line of evidence (STRING path); GRN and Reactome corroborate none; 22/93 paths run through a promiscuous hub. The CRISPR phenotype is the only causal fact — STRING proposes candidate wiring to be tested (§5). And 49 genes remain a genuine knowledge gap: causal for melanin, unwired to canonical machinery even under the symmetric pull. Both halves are the finding.