import matplotlib as mpl, matplotlib.pyplot as plt
from pathlib import Path
from IPython.display import display
Path("output/figures").mkdir(parents=True, exist_ok=True)
# self-contained publication styling (no external skill dependency)
mpl.rcParams.update({
"font.size": 8, "axes.titlesize": 8, "axes.labelsize": 8,
"xtick.labelsize": 6, "ytick.labelsize": 6, "legend.fontsize": 6,
"axes.spines.top": False, "axes.spines.right": False,
"xtick.direction": "out", "ytick.direction": "out", "savefig.dpi": 300,
})
GREY = "#6e6e6e"
TYPE_ORDER = ["pending_db_resolution", "complex", "environmental"]
TYPE_COLS = ["#4c72b0", "#55a868", "#c44e52"]
SIGN_ORDER = ["+", "-", "0"]
SIGN_COLS = {"+": "#2c7fb8", "-": "#d95f0e", "0": "#999999"}
edges_df = pd.read_csv("data/processed/raghunath_edges_typed_signed.csv")
nodes_df = pd.read_csv("data/processed/raghunath_nodes_typed.csv")
# degree cross-check vs published Additional File 2
m2 = pd.read_excel(MOESM2, sheet_name="node_properties", header=1)
m2.columns = [str(c).strip() for c in m2.columns]
pub = m2[["Node", "Indegree", "Outdegree"]].dropna(subset=["Node"]).copy()
pub["Node"] = fix_labels(pub["Node"].str.strip()); pub = pub[pub["Node"] != "Node"] # same source-typo correction
for c in ["Indegree", "Outdegree"]:
pub[c] = pd.to_numeric(pub[c], errors="coerce")
outdeg = edges_df.groupby("source").size(); indeg = edges_df.groupby("target").size()
pub["our_in"] = pub["Node"].map(indeg).fillna(0).astype(int)
pub["our_out"] = pub["Node"].map(outdeg).fillna(0).astype(int)
pub["pub_total"] = pub["Indegree"] + pub["Outdegree"]
pub["our_total"] = pub["our_in"] + pub["our_out"]
nmatch = int((pub["pub_total"] == pub["our_total"]).sum())
assert nmatch == len(pub), f"degree mismatch: only {nmatch}/{len(pub)} nodes agree with Additional File 2"
print(f"degree cross-check: {nmatch}/{len(pub)} nodes match published in/out-degree exactly")
tot = (edges_df.groupby("source").size().add(edges_df.groupby("target").size(), fill_value=0)).astype(int)
fig, axs = plt.subplots(2, 2, figsize=(7.2, 6.4)); axA, axB, axC, axD = axs.ravel()
mx = int(pub[["pub_total", "our_total"]].to_numpy().max()) + 2
axA.plot([0, mx], [0, mx], color=GREY, lw=1, zorder=1)
axA.scatter(pub["pub_total"], pub["our_total"], s=16, color=TYPE_COLS[0],
edgecolor="white", linewidth=0.3, alpha=0.8, zorder=2)
axA.set_xlim(-1, mx); axA.set_ylim(-1, mx)
axA.set_xlabel("Published total degree (Add. File 2)"); axA.set_ylabel("Recomputed total degree")
axA.set_title(f"Degree matches published for all {nmatch}/{len(pub)} nodes", loc="left")
axA.text(mx*0.62, mx*0.72, "y = x", color=GREY, fontsize=6, rotation=45, va="center")
tc = nodes_df["node_type"].value_counts().reindex(TYPE_ORDER).fillna(0).astype(int)
axB.bar(range(len(tc)), tc.values, color=TYPE_COLS, width=0.62)
for i, v in enumerate(tc.values): axB.text(i, v+4, str(v), ha="center", va="bottom", fontsize=7)
axB.set_xticks(range(len(tc))); axB.set_xticklabels(["pending\n(→ NB2)", "complex\n(colon)", "environmental\n(UVA/UVB)"], fontsize=6)
axB.set_ylabel("Nodes"); axB.set_ylim(0, tc.max()*1.15)
axB.set_title("Node types: most (239) await gene resolution", loc="left")
sc = edges_df["sign"].value_counts().reindex(SIGN_ORDER).fillna(0).astype(int)
axC.bar(range(len(sc)), sc.values, color=[SIGN_COLS[s] for s in SIGN_ORDER], width=0.62)
for i, v in enumerate(sc.values): axC.text(i, v+5, str(v), ha="center", va="bottom", fontsize=7)
axC.set_xticks(range(len(sc))); axC.set_xticklabels(["+ activating /\nexpression", "− inhibiting /\ndegrading", "0 direction-\nless"], fontsize=6)
axC.set_ylabel("Edges"); axC.set_ylim(0, sc.max()*1.15)
axC.set_title("429 edges signed from explicit verb table", loc="left")
vc = tot.value_counts().sort_index()
axD.scatter(vc.index, vc.values, s=18, color=TYPE_COLS[1], edgecolor="white", linewidth=0.3)
axD.set_yscale("log"); axD.set_xlabel("Total degree"); axD.set_ylabel("Number of nodes")
axD.set_title("Few hubs, many leaves", loc="left")
top = tot.sort_values(ascending=False).head(3)
hub_txt = "Top hubs (degree):\n" + "\n".join(f" {n.replace('_',' ')} — {int(d)}" for n, d in top.items())
axD.text(0.97, 0.95, hub_txt, transform=axD.transAxes, ha="right", va="top", fontsize=6, color=GREY)
axD.set_ylim(0.7, vc.max()*1.6)
for ax, L in zip([axA, axB, axC, axD], "abcd"):
ax.text(-0.18, 1.02, L, transform=ax.transAxes, fontweight="bold", fontsize=10, va="bottom")
fig.suptitle("Notebook 1 — sourced reproduction of Raghunath (2015): 265 nodes / 429 edges",
fontsize=8, x=0.01, ha="left", weight="bold")
fig.tight_layout(rect=[0, 0, 1, 0.965])
fig.savefig("output/figures/nb1_validation_dashboard.png", bbox_inches="tight")
print("wrote output/figures/nb1_validation_dashboard.png")
display(fig) # embed the figure in the notebook's stored output so it renders on the Quarto site (execute: false)
plt.close(fig) # ensures exactly one embedded copy regardless of the active matplotlib backend