Skip to content

Commit 8ddfe18

Browse files
committed
docs: DiffBio manuscript on jointly optimized preprocessing
Add the manuscript, figures, and references: the single-cell annotation result characterized in depth, extended with cross-modality and cross-machinery evidence (DNA, Perturb-seq, scATAC, variant calling) and a no-bottleneck null that pins the effect to lossy reduction that discards task signal.
1 parent a3950f7 commit 8ddfe18

9 files changed

Lines changed: 1089 additions & 0 deletions

‎paper/.gitignore‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# LaTeX build artifacts
2+
*.aux
3+
*.bbl
4+
*.blg
5+
*.log
6+
*.out
7+
*.toc
8+
*.fdb_latexmk
9+
*.fls
10+
*.synctex.gz
11+
*.png
12+
b1.log
13+
b2.log
14+
b3.log
15+
bib.log
16+
bibtex.log
17+
build*.log

‎paper/diffbio.pdf‎

542 KB
Binary file not shown.

‎paper/diffbio.tex‎

Lines changed: 607 additions & 0 deletions
Large diffs are not rendered by default.
21.8 KB
Binary file not shown.

‎paper/figures/fig_gain_vs_k.pdf‎

20 KB
Binary file not shown.

‎paper/figures/fig_main_result.pdf‎

18.5 KB
Binary file not shown.
19.5 KB
Binary file not shown.

‎paper/make_figures.py‎

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
"""Generate the manuscript figures from the DiffBio Gate-2 benchmark results.
2+
3+
All numbers are the summary statistics (mean +/- std over 3 seeds unless noted) of the
4+
runs described in the paper; each block cites its source run. Figures are written as
5+
vector PDFs into ``paper/figures/`` for inclusion in the LaTeX manuscript.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
from pathlib import Path
11+
12+
import matplotlib as mpl
13+
import matplotlib.pyplot as plt
14+
import numpy as np
15+
16+
mpl.rcParams.update(
17+
{
18+
"figure.dpi": 150,
19+
"savefig.bbox": "tight",
20+
"font.family": "serif",
21+
"font.size": 10,
22+
"axes.spines.top": False,
23+
"axes.spines.right": False,
24+
"axes.grid": True,
25+
"grid.alpha": 0.25,
26+
"grid.linewidth": 0.5,
27+
}
28+
)
29+
30+
FIG_DIR = Path(__file__).resolve().parent / "figures"
31+
FIG_DIR.mkdir(parents=True, exist_ok=True)
32+
33+
FROZEN = "#4C72B0"
34+
JOINT = "#C44E52"
35+
ACCENT = "#55A868"
36+
37+
# --- Main Gate-2 result: macro-F1 and rare-type F1, frozen vs joint --------------
38+
# Source runs: gate2_joint_pipeline.json (TS120k dispersion); gate2_ts120k_supervised,
39+
# gate2_tsall_dispersion, gate2_tsall_supervised (gate2_strengthen.py).
40+
CONDITIONS = [
41+
# label, frozen_macro, frozen_std, joint_macro, joint_std, frozen_rare, joint_rare
42+
("TS-120k\n59 types\ndispersion", 0.7304, 0.0054, 0.7826, 0.0034, 0.563, 0.625),
43+
("TS-120k\n59 types\nsupervised", 0.7398, 0.0134, 0.7989, 0.0000, 0.595, 0.648),
44+
("TS-193k\n87 types\ndispersion", 0.6497, 0.0060, 0.7518, 0.0000, 0.403, 0.534),
45+
]
46+
47+
48+
def fig_main_result() -> None:
49+
"""Grouped bars: frozen vs joint, overall macro-F1 and rare-type F1."""
50+
labels = [c[0] for c in CONDITIONS]
51+
x = np.arange(len(labels))
52+
width = 0.38
53+
fig, axes = plt.subplots(1, 2, figsize=(9.0, 3.6))
54+
55+
frozen = [c[1] for c in CONDITIONS]
56+
frozen_err = [c[2] for c in CONDITIONS]
57+
joint = [c[3] for c in CONDITIONS]
58+
joint_err = [c[4] for c in CONDITIONS]
59+
axes[0].bar(
60+
x - width / 2, frozen, width, yerr=frozen_err, capsize=3, label="Frozen PCA", color=FROZEN
61+
)
62+
axes[0].bar(
63+
x + width / 2,
64+
joint,
65+
width,
66+
yerr=joint_err,
67+
capsize=3,
68+
label="Joint projection",
69+
color=JOINT,
70+
)
71+
for xi, (f, j) in enumerate(zip(frozen, joint, strict=True)):
72+
axes[0].annotate(
73+
f"+{100 * (j - f):.1f}", (xi, max(f, j) + 0.02), ha="center", fontsize=8, color=JOINT
74+
)
75+
axes[0].set_ylabel("Held-out macro-F1")
76+
axes[0].set_title("(a) Cell-type annotation accuracy")
77+
axes[0].set_ylim(0.55, 0.87)
78+
79+
frozen_rare = [c[5] for c in CONDITIONS]
80+
joint_rare = [c[6] for c in CONDITIONS]
81+
axes[1].bar(x - width / 2, frozen_rare, width, label="Frozen PCA", color=FROZEN)
82+
axes[1].bar(x + width / 2, joint_rare, width, label="Joint projection", color=JOINT)
83+
for xi, (f, j) in enumerate(zip(frozen_rare, joint_rare, strict=True)):
84+
axes[1].annotate(
85+
f"+{100 * (j - f):.1f}", (xi, max(f, j) + 0.015), ha="center", fontsize=8, color=JOINT
86+
)
87+
axes[1].set_ylabel("Rare cell-type F1")
88+
axes[1].set_title("(b) Rare cell types")
89+
axes[1].set_ylim(0.35, 0.72)
90+
91+
for ax in axes:
92+
ax.set_xticks(x)
93+
ax.set_xticklabels(labels, fontsize=7.5)
94+
ax.legend(frameon=False, fontsize=8, loc="upper right")
95+
fig.tight_layout()
96+
fig.savefig(FIG_DIR / "fig_main_result.pdf")
97+
plt.close(fig)
98+
99+
100+
def fig_gain_vs_k() -> None:
101+
"""The central regime figure: frozen and joint macro-F1 as a function of k."""
102+
# Matched frozen/joint macro-F1 across k (TS-120k, dispersion HVG, 3 seeds).
103+
k_values = [5, 10, 20, 50]
104+
frozen = {5: (0.5645, 0.0108), 10: (0.7304, 0.0054), 20: (0.7941, 0.0070), 50: (0.8243, 0.0111)}
105+
joint = {5: (0.7488, 0.0077), 10: (0.7826, 0.0034), 20: (0.8211, 0.0064), 50: (0.8406, 0.0035)}
106+
soft = (0.8304, 40.3)
107+
108+
fig, ax = plt.subplots(figsize=(5.4, 4.0))
109+
ks = np.array(k_values)
110+
fm = np.array([frozen[k][0] for k in k_values])
111+
fs = np.array([frozen[k][1] for k in k_values])
112+
ax.plot(ks, fm, "-o", color=FROZEN, label="Frozen PCA")
113+
ax.fill_between(ks, fm - fs, fm + fs, color=FROZEN, alpha=0.15)
114+
115+
jk = sorted(joint)
116+
jm = np.array([joint[k][0] for k in jk])
117+
js = np.array([joint[k][1] for k in jk])
118+
ax.plot(jk, jm, "-s", color=JOINT, label="Joint projection")
119+
ax.fill_between(jk, jm - js, jm + js, color=JOINT, alpha=0.15)
120+
121+
ax.plot(
122+
[soft[1]],
123+
[soft[0]],
124+
"*",
125+
color=ACCENT,
126+
markersize=14,
127+
label=f"Soft-k (learned dim {soft[1]:.0f})",
128+
)
129+
for k in jk:
130+
gain = 100 * (joint[k][0] - frozen[k][0])
131+
ax.annotate(
132+
f"+{gain:.1f}pp", (k, joint[k][0] + 0.006), ha="center", fontsize=8, color=JOINT
133+
)
134+
ax.set_xscale("log")
135+
ax.set_xticks(k_values)
136+
ax.get_xaxis().set_major_formatter(mpl.ticker.ScalarFormatter())
137+
ax.set_xlabel("Number of components $k$")
138+
ax.set_ylabel("Held-out macro-F1")
139+
ax.set_title("Joint gain concentrates at aggressive reduction")
140+
ax.legend(frameon=False, fontsize=8, loc="lower right")
141+
fig.tight_layout()
142+
fig.savefig(FIG_DIR / "fig_gain_vs_k.pdf")
143+
plt.close(fig)
144+
145+
146+
def fig_scaling_and_ablation() -> None:
147+
"""Gain vs dataset size (crossover) and the knob-attribution ablation."""
148+
fig, axes = plt.subplots(1, 2, figsize=(9.0, 3.6))
149+
150+
# (a) gain vs n_cells; source: joint-preprocessing scaling curve + Gate-2 runs.
151+
n_cells = np.array([2000, 10000, 33000, 120000, 193000])
152+
gain = np.array([-0.086, -0.027, -0.007, 0.052, 0.102])
153+
axes[0].axhline(0.0, color="grey", lw=0.8, ls="--")
154+
axes[0].plot(n_cells, gain, "-o", color=JOINT)
155+
axes[0].fill_between(n_cells, gain, 0, where=(gain > 0), color=JOINT, alpha=0.12)
156+
axes[0].fill_between(n_cells, gain, 0, where=(gain <= 0), color=FROZEN, alpha=0.12)
157+
axes[0].set_xscale("log")
158+
axes[0].set_xlabel("Atlas size (cells)")
159+
axes[0].set_ylabel("Joint $-$ frozen macro-F1")
160+
axes[0].set_title("(a) Gain grows with scale (crossover)")
161+
axes[0].annotate("frozen wins\n(small data)", (2600, -0.07), fontsize=7.5, color=FROZEN)
162+
axes[0].annotate("joint wins\n(atlas scale)", (60000, 0.07), fontsize=7.5, color=JOINT)
163+
164+
# (b) ablation: which learnable knob carries the gain (TS-120k, k=10, 2 seeds).
165+
variants = ["frozen", "norm", "hvg", "proj", "all"]
166+
scores = [0.736, 0.734, 0.735, 0.765, 0.765]
167+
colors = [FROZEN, FROZEN, FROZEN, JOINT, JOINT]
168+
axes[1].bar(variants, scores, color=colors)
169+
axes[1].set_ylim(0.72, 0.775)
170+
axes[1].axhline(scores[0], color="grey", lw=0.8, ls="--")
171+
axes[1].set_ylabel("Held-out macro-F1")
172+
axes[1].set_title("(b) The projection carries the gain")
173+
axes[1].annotate("learnable\nprojection", (3, 0.767), ha="center", fontsize=7.5, color=JOINT)
174+
fig.tight_layout()
175+
fig.savefig(FIG_DIR / "fig_scaling_ablation.pdf")
176+
plt.close(fig)
177+
178+
179+
def fig_cross_modality() -> None:
180+
"""The joint gain at aggressive reduction across pipelines, modalities and machineries."""
181+
# (label, gain_pp @ k=5 unless noted, machinery). Sources: gate_sequence/gate_vcc/
182+
# gate_atac/gate_variant_pool JSON summaries (3 seeds each) and the scRNA gain-vs-k run.
183+
entries = [
184+
("scRNA\nannotation", 18.4, "linear"),
185+
("DNA regulatory\nelements", 7.5, "linear"),
186+
("Perturb-seq\nidentity", 8.3, "linear"),
187+
("scATAC\nannotation", 12.2, "linear"),
188+
("Variant calling\n(read pooling)", 16.9, "pool"),
189+
("Variant calling\n(no bottleneck)", -1.1, "null"),
190+
]
191+
color = {"linear": JOINT, "pool": ACCENT, "null": "#9AA0A6"}
192+
193+
fig, ax = plt.subplots(figsize=(7.4, 3.6))
194+
positions = np.arange(len(entries))
195+
gains = [entry[1] for entry in entries]
196+
colors = [color[entry[2]] for entry in entries]
197+
ax.bar(positions, gains, color=colors, width=0.66)
198+
ax.axhline(0.0, color="grey", lw=0.8)
199+
for position, gain in zip(positions, gains, strict=True):
200+
offset = 0.5 if gain >= 0 else -0.9
201+
ax.annotate(f"{gain:+.1f}", (float(position), gain + offset), ha="center", fontsize=8)
202+
ax.set_xticks(positions)
203+
ax.set_xticklabels([entry[0] for entry in entries], fontsize=7.5)
204+
ax.set_ylabel("Joint $-$ frozen macro-F1 (pp)")
205+
ax.set_title("The joint gain generalizes across modalities and machineries")
206+
handles = [
207+
mpl.patches.Patch(color=JOINT, label="Linear projection"),
208+
mpl.patches.Patch(color=ACCENT, label="Set pooling (reads)"),
209+
mpl.patches.Patch(color="#9AA0A6", label="No reduction bottleneck"),
210+
]
211+
ax.legend(
212+
handles=handles, frameon=False, fontsize=8, loc="upper center",
213+
ncol=3, bbox_to_anchor=(0.5, 1.0), columnspacing=1.4, handletextpad=0.5,
214+
)
215+
ax.set_ylim(-4, 25)
216+
fig.tight_layout()
217+
fig.savefig(FIG_DIR / "fig_cross_modality.pdf")
218+
plt.close(fig)
219+
220+
221+
def main() -> None:
222+
"""Generate all manuscript figures."""
223+
fig_main_result()
224+
fig_gain_vs_k()
225+
fig_scaling_and_ablation()
226+
fig_cross_modality()
227+
print(f"figures written to {FIG_DIR}")
228+
229+
230+
if __name__ == "__main__":
231+
main()

0 commit comments

Comments
 (0)