π§ Key takeaways
Multiome data allow regions to be linked to genes and TFs to regions in the same cells, but the resulting networks are hypotheses that differ between methods.
Region-gene links combine genomic proximity with the correlation of accessibility and expression, and TF-region links require a binding motif.
Only a small fraction of inferred links is supported by prior knowledge, so networks need validation, ideally by perturbing the TFs.
βοΈ Environment setup
Install conda:
Before creating the environment, ensure that conda is installed on your system.
Save the yml content:
Copy the content from the yml tab into a file named
environment.yml.
Create the environment:
Open a terminal or command prompt.
Run the following command:
conda env create -f environment.yml
Activate the environment:
After the environment is created, activate it using:
conda activate <environment_name>Replace
<environment_name>with the name specified in theenvironment.ymlfile. In the yml file it will look like this:name: <environment_name>
Verify the installation:
Check that the environment was created successfully by running:
conda env list
name: chromatin-accessibility
channels:
- conda-forge
dependencies:
- conda-forge::ipykernel=7.3.0
- conda-forge::ipywidgets=8.1.9
- conda-forge::python=3.13.15
- conda-forge::scanpy=1.12.4
- pip
- pip:
- lamindb==2.10.0
- macs3==3.0.5
- muon==0.1.9
- snapatac2==2.10.0
ποΈ Get data and notebooks
This book uses lamindb to store, share, and load datasets and notebooks using the theislab/sc-best-practices instance. We acknowledge free hosting from Lamin Labs.
Install lamindb
Install the lamindb Python package:
pip install lamindbOptionally create a lamin account
Sign up and log in following the instructions
Verify your setup
Run the
lamin connectcommand:
import lamindb as ln ln.Artifact.connect("theislab/sc-best-practices").df()You should now see up to 100 of the stored datasets.
Accessing datasets (Artifacts)
Search for the datasets on the Artifacts page
Load an Artifact and the corresponding object:
import lamindb as ln af = ln.Artifact.connect("theislab/sc-best-practices").get( key="key_of_dataset", is_latest=True ) obj = af.load()The object is now accessible in memory and is ready for analysis. Adapt the
lamindb.Artifact.connect("theislab/sc-best-practices").get("SOMEIDXXXX")suffix to get respective versions.Accessing notebooks (Transforms)
Search for the notebook on the Transforms page
Load the notebook:
lamin load <notebook url>which will download the notebook to the current working directory. Analogously to
Artifacts, you can adapt the suffix ID to get older versions.
MotivationΒΆ
Transcription factors (TFs) regulate genes by binding to accessible regulatory regions, such as promoters and enhancers, whose accessibility changes with their activity Spitz & Furlong, 2012. Multiome data measure accessibility and expression in the same cells, which allows us to link regions to the genes whose expression follows their accessibility, and TFs to the regions that contain their motifs. Together, these links form a network from TFs through regions to target genes, the idea behind methods such as SCENIC+ Bravo GonzΓ‘lez-Blas et al., 2023.
Such networks are hypotheses rather than measurements: the networks of different methods barely overlap, change with random seeds and preprocessing, and predict the effects of perturbations poorly Mompel et al., 2024. For TF activities from gene expression alone, see the gene regulatory network chapter of the RNA section.
import lamindb as ln
import pandas as pd
import scanpy as sc
import snapatac2 as snap
ln.connect("theislab/sc-best-practices")
ln.track("N64Xsy5YJqH4")Output
β connected lamindb: theislab/sc-best-practices
β loaded Transform('N64Xsy5YJqH40000', key='gene_regulatory_networks_atac.ipynb'), started new Run('Zh98aN728L2pCIMK') at 2026-09-29 15:44:48 UTC
β notebook imports: lamindb-core==2.10.0 pandas==2.3.3 scanpy==1.12.4 snapatac2==2.10.0
We load the peak matrix from the previous chapter and the gene expression of the same cells, and normalize both with the shifted logarithm. We restrict the network to the cell type-specific peaks and to the 2,000 most variable genes, which keeps it small and focused on regulation that differs between cell types.
peak_matrix = ln.Artifact.get(key="chromatin_accessibility/pbmc10k_peaks.h5ad").load()
rna = ln.Artifact.get(
key="introduction/advanced_data_structures_and_frameworks_muon.h5mu"
).load()["rna"]
rna = rna[peak_matrix.obs_names].copy()
rna.var_names_make_unique()
for data in (peak_matrix, rna):
sc.pp.normalize_total(data)
sc.pp.log1p(data)
sc.pp.highly_variable_genes(rna, n_top_genes=2000)
markers = snap.tl.marker_regions(peak_matrix, groupby="cell_type", pvalue=0.01)
regions = sorted({region for peaks in markers.values() for region in peaks})Linking regions to genesΒΆ
We connect every region to the genes whose transcription start site lies within 250 kb and keep the links in which accessibility and expression are correlated across cells.
annotation = ln.Artifact.get(
key="chromatin_accessibility/gencode_v41_GRCh38.gff3.gz"
).cache()
network = snap.tl.init_network_from_annotation(regions, annotation)
variable_genes = set(rna.var_names[rna.var["highly_variable"]])
network = snap.tl.prune_network(
network, node_filter=lambda node: node.type != "gene" or node.id in variable_genes
)
snap.tl.add_cor_scores(network, gene_mat=rna, peak_mat=peak_matrix)
network = snap.tl.prune_network(
network, edge_filter=lambda source, target, link: link.cor_score > 0.1
)Output
2026-09-29 17:45:03 - INFO - Removed 22374 isolated nodes.
0%| | 0/1204 [00:00<?, ?it/s] 4%|β | 44/1204 [00:00<00:02, 432.77it/s] 7%|β | 89/1204 [00:00<00:02, 436.39it/s] 11%|β | 133/1204 [00:00<00:02, 412.37it/s] 15%|ββ | 178/1204 [00:00<00:02, 425.95it/s] 18%|ββ | 222/1204 [00:00<00:02, 428.96it/s] 22%|βββ | 265/1204 [00:00<00:02, 428.19it/s] 26%|βββ | 310/1204 [00:00<00:02, 431.96it/s] 29%|βββ | 354/1204 [00:00<00:02, 415.27it/s] 33%|ββββ | 400/1204 [00:00<00:01, 428.47it/s] 37%|ββββ | 444/1204 [00:01<00:01, 426.45it/s] 41%|ββββ | 489/1204 [00:01<00:01, 430.75it/s] 44%|βββββ | 533/1204 [00:01<00:01, 428.78it/s] 48%|βββββ | 576/1204 [00:01<00:01, 426.61it/s] 52%|ββββββ | 622/1204 [00:01<00:01, 431.36it/s] 55%|ββββββ | 667/1204 [00:01<00:01, 434.01it/s] 59%|ββββββ | 711/1204 [00:01<00:01, 419.47it/s] 63%|βββββββ | 755/1204 [00:01<00:01, 424.42it/s] 66%|βββββββ | 798/1204 [00:01<00:00, 424.21it/s] 70%|βββββββ | 841/1204 [00:01<00:00, 424.86it/s] 74%|ββββββββ | 890/1204 [00:02<00:00, 440.00it/s] 78%|ββββββββ | 935/1204 [00:02<00:00, 422.31it/s] 81%|ββββββββ | 978/1204 [00:02<00:00, 401.90it/s] 85%|βββββββββ | 1021/1204 [00:02<00:00, 408.67it/s] 88%|βββββββββ | 1064/1204 [00:02<00:00, 413.80it/s] 92%|ββββββββββ| 1108/1204 [00:02<00:00, 420.65it/s] 96%|ββββββββββ| 1152/1204 [00:02<00:00, 425.19it/s] 99%|ββββββββββ| 1195/1204 [00:02<00:00, 426.13it/s]100%|ββββββββββ| 1204/1204 [00:02<00:00, 424.78it/s]
2026-09-29 17:45:06 - INFO - Removed 10238 isolated nodes.
f"{network.num_edges()} region-gene links"'541 region-gene links'Adding transcription factorsΒΆ
Next, we connect TFs to the linked regions that contain their CIS-BP motif Weirauch et al., 2014 and derive TF-gene links through these regions Zhang et al., 2024. A motif only shows that a TF could bind, so we additionally require that the TF and its target are co-expressed, and we remove links of TFs to their own gene, whose correlation is trivially 1.
fasta = ln.Artifact.get(key="chromatin_accessibility/gencode_v41_GRCh38.fa").cache()
motif_file = ln.Artifact.get(key="chromatin_accessibility/cisBP_human.meme").cache()
cis_bp = {}
for motif in snap.read_motifs(str(motif_file)):
motif.name = motif.id.split("+")[0]
if (
motif.name not in cis_bp
or motif.info_content() > cis_bp[motif.name].info_content()
):
cis_bp[motif.name] = motif
snap.tl.add_tf_binding(network, motifs=list(cis_bp.values()), genome_fasta=fasta)
grn = snap.tl.link_tf_to_gene(network)
snap.tl.add_cor_scores(grn, gene_mat=rna, peak_mat=peak_matrix)
grn = snap.tl.prune_network(
grn,
edge_filter=lambda source, target, link: source != target and link.cor_score > 0.1,
)
edges = pd.DataFrame(
[
(grn[source].id, grn[target].id, grn.get_edge_data(source, target).cor_score)
for source, target in grn.edge_list()
],
columns=["tf", "target", "correlation"],
)Output
2026-09-29 17:45:07 - INFO - Fetching 524 sequences ...
2026-09-29 17:45:07 - INFO - Searching for the binding sites of 1165 motifs ...
0%| | 0/1165 [00:00<?, ?it/s] 1%| | 7/1165 [00:00<00:18, 62.18it/s] 2%|β | 24/1165 [00:00<00:09, 121.47it/s] 3%|β | 37/1165 [00:00<00:12, 92.85it/s] 4%|β | 48/1165 [00:00<00:12, 92.22it/s] 5%|β | 64/1165 [00:00<00:09, 110.39it/s] 7%|β | 76/1165 [00:00<00:09, 111.01it/s] 8%|β | 88/1165 [00:00<00:10, 101.16it/s] 9%|β | 103/1165 [00:00<00:09, 113.91it/s] 10%|β | 115/1165 [00:01<00:09, 112.72it/s] 11%|β | 127/1165 [00:01<00:10, 100.78it/s] 12%|ββ | 138/1165 [00:01<00:11, 92.49it/s] 13%|ββ | 151/1165 [00:01<00:10, 97.94it/s] 14%|ββ | 162/1165 [00:01<00:10, 93.99it/s] 15%|ββ | 172/1165 [00:01<00:11, 89.16it/s] 16%|ββ | 182/1165 [00:01<00:11, 83.67it/s] 16%|ββ | 191/1165 [00:02<00:13, 72.41it/s] 17%|ββ | 199/1165 [00:02<00:14, 67.03it/s] 18%|ββ | 209/1165 [00:02<00:12, 74.15it/s] 19%|ββ | 220/1165 [00:02<00:11, 80.19it/s] 20%|ββ | 234/1165 [00:02<00:09, 94.87it/s] 21%|ββ | 244/1165 [00:02<00:10, 88.09it/s] 22%|βββ | 254/1165 [00:02<00:12, 71.89it/s] 23%|βββ | 268/1165 [00:02<00:10, 87.04it/s] 24%|βββ | 281/1165 [00:03<00:09, 93.03it/s] 25%|βββ | 292/1165 [00:03<00:09, 96.61it/s] 26%|βββ | 303/1165 [00:03<00:09, 90.01it/s] 28%|βββ | 324/1165 [00:03<00:07, 118.13it/s] 29%|βββ | 340/1165 [00:03<00:06, 127.69it/s] 30%|βββ | 354/1165 [00:03<00:07, 104.92it/s] 31%|ββββ | 366/1165 [00:04<00:10, 76.38it/s] 32%|ββββ | 377/1165 [00:04<00:09, 81.88it/s] 33%|ββββ | 387/1165 [00:04<00:09, 79.50it/s] 35%|ββββ | 404/1165 [00:04<00:07, 99.05it/s] 36%|ββββ | 416/1165 [00:04<00:07, 100.76it/s] 37%|ββββ | 428/1165 [00:04<00:08, 91.70it/s] 38%|ββββ | 438/1165 [00:04<00:08, 88.14it/s] 39%|ββββ | 450/1165 [00:04<00:07, 94.92it/s] 40%|ββββ | 461/1165 [00:05<00:08, 87.91it/s] 41%|ββββ | 475/1165 [00:05<00:07, 97.71it/s] 42%|βββββ | 486/1165 [00:05<00:07, 90.98it/s] 43%|βββββ | 496/1165 [00:05<00:07, 92.22it/s] 43%|βββββ | 506/1165 [00:05<00:07, 83.89it/s] 45%|βββββ | 521/1165 [00:05<00:06, 99.24it/s] 46%|βββββ | 532/1165 [00:05<00:08, 76.21it/s] 46%|βββββ | 541/1165 [00:05<00:08, 71.42it/s] 47%|βββββ | 549/1165 [00:06<00:08, 71.09it/s] 48%|βββββ | 560/1165 [00:06<00:08, 75.28it/s] 49%|βββββ | 568/1165 [00:06<00:09, 62.02it/s] 50%|βββββ | 580/1165 [00:06<00:07, 74.30it/s] 51%|βββββ | 593/1165 [00:06<00:06, 86.86it/s] 52%|ββββββ | 603/1165 [00:06<00:07, 74.31it/s] 53%|ββββββ | 612/1165 [00:06<00:07, 70.86it/s] 53%|ββββββ | 620/1165 [00:07<00:08, 66.32it/s] 54%|ββββββ | 628/1165 [00:07<00:07, 67.22it/s] 55%|ββββββ | 636/1165 [00:07<00:08, 65.49it/s] 56%|ββββββ | 647/1165 [00:07<00:07, 70.01it/s] 56%|ββββββ | 655/1165 [00:07<00:07, 67.49it/s] 57%|ββββββ | 663/1165 [00:07<00:07, 69.12it/s] 58%|ββββββ | 676/1165 [00:07<00:05, 82.86it/s] 59%|ββββββ | 689/1165 [00:07<00:05, 94.49it/s] 60%|ββββββ | 699/1165 [00:08<00:04, 93.52it/s] 61%|ββββββ | 709/1165 [00:08<00:05, 85.12it/s] 62%|βββββββ | 718/1165 [00:08<00:05, 79.42it/s] 62%|βββββββ | 727/1165 [00:08<00:05, 77.17it/s] 63%|βββββββ | 735/1165 [00:08<00:06, 70.79it/s] 64%|βββββββ | 743/1165 [00:08<00:06, 66.25it/s] 65%|βββββββ | 753/1165 [00:08<00:05, 74.15it/s] 66%|βββββββ | 767/1165 [00:08<00:04, 90.67it/s] 67%|βββββββ | 777/1165 [00:09<00:04, 86.06it/s] 67%|βββββββ | 786/1165 [00:09<00:05, 69.10it/s] 68%|βββββββ | 794/1165 [00:09<00:05, 63.83it/s] 69%|βββββββ | 802/1165 [00:09<00:05, 67.20it/s] 70%|βββββββ | 811/1165 [00:09<00:04, 71.99it/s] 70%|βββββββ | 819/1165 [00:09<00:04, 73.82it/s] 71%|βββββββ | 828/1165 [00:09<00:04, 76.05it/s] 72%|ββββββββ | 836/1165 [00:10<00:05, 64.66it/s] 73%|ββββββββ | 848/1165 [00:10<00:04, 77.73it/s] 74%|ββββββββ | 857/1165 [00:10<00:04, 68.31it/s] 74%|ββββββββ | 865/1165 [00:10<00:04, 63.66it/s] 75%|ββββββββ | 872/1165 [00:10<00:05, 55.24it/s] 75%|ββββββββ | 879/1165 [00:10<00:05, 56.62it/s] 76%|ββββββββ | 885/1165 [00:10<00:05, 49.65it/s] 76%|ββββββββ | 891/1165 [00:11<00:06, 42.48it/s] 77%|ββββββββ | 896/1165 [00:11<00:06, 43.98it/s] 77%|ββββββββ | 901/1165 [00:11<00:06, 39.32it/s] 78%|ββββββββ | 906/1165 [00:11<00:06, 40.87it/s] 79%|ββββββββ | 920/1165 [00:11<00:04, 58.51it/s] 79%|ββββββββ | 926/1165 [00:11<00:04, 52.78it/s] 80%|ββββββββ | 932/1165 [00:11<00:04, 52.13it/s] 81%|ββββββββ | 938/1165 [00:12<00:04, 45.78it/s] 81%|ββββββββ | 943/1165 [00:12<00:04, 44.48it/s] 81%|βββββββββ | 948/1165 [00:12<00:05, 43.01it/s] 82%|βββββββββ | 953/1165 [00:12<00:05, 38.45it/s] 82%|βββββββββ | 957/1165 [00:12<00:05, 37.33it/s] 82%|βββββββββ | 961/1165 [00:12<00:05, 37.27it/s] 83%|βββββββββ | 965/1165 [00:12<00:05, 37.26it/s] 83%|βββββββββ | 969/1165 [00:12<00:05, 33.19it/s] 84%|βββββββββ | 975/1165 [00:13<00:04, 39.74it/s] 84%|βββββββββ | 980/1165 [00:13<00:04, 40.05it/s] 85%|βββββββββ | 985/1165 [00:13<00:04, 37.65it/s] 85%|βββββββββ | 990/1165 [00:13<00:04, 38.64it/s] 85%|βββββββββ | 994/1165 [00:13<00:04, 37.96it/s] 86%|βββββββββ | 999/1165 [00:13<00:04, 38.75it/s] 86%|βββββββββ | 1003/1165 [00:13<00:04, 38.56it/s] 87%|βββββββββ | 1008/1165 [00:13<00:03, 40.32it/s] 87%|βββββββββ | 1013/1165 [00:14<00:03, 40.22it/s] 87%|βββββββββ | 1018/1165 [00:14<00:03, 42.16it/s] 88%|βββββββββ | 1023/1165 [00:14<00:03, 40.10it/s] 88%|βββββββββ | 1029/1165 [00:14<00:03, 45.13it/s] 89%|βββββββββ | 1036/1165 [00:14<00:02, 50.25it/s] 89%|βββββββββ | 1042/1165 [00:14<00:02, 45.34it/s] 90%|βββββββββ | 1047/1165 [00:14<00:02, 41.17it/s] 90%|βββββββββ | 1052/1165 [00:14<00:02, 39.43it/s] 91%|βββββββββ | 1057/1165 [00:15<00:02, 40.95it/s] 91%|ββββββββββ| 1064/1165 [00:15<00:02, 45.63it/s] 92%|ββββββββββ| 1071/1165 [00:15<00:01, 49.27it/s] 92%|ββββββββββ| 1077/1165 [00:15<00:01, 48.06it/s] 93%|ββββββββββ| 1082/1165 [00:15<00:01, 47.94it/s] 93%|ββββββββββ| 1087/1165 [00:15<00:01, 47.09it/s] 94%|ββββββββββ| 1095/1165 [00:15<00:01, 55.77it/s] 95%|ββββββββββ| 1101/1165 [00:15<00:01, 51.01it/s] 95%|ββββββββββ| 1110/1165 [00:16<00:00, 59.49it/s] 96%|ββββββββββ| 1117/1165 [00:16<00:00, 54.37it/s] 96%|ββββββββββ| 1123/1165 [00:16<00:00, 49.88it/s] 97%|ββββββββββ| 1129/1165 [00:16<00:00, 47.47it/s] 98%|ββββββββββ| 1137/1165 [00:16<00:00, 52.87it/s] 98%|ββββββββββ| 1143/1165 [00:16<00:00, 45.61it/s] 99%|ββββββββββ| 1149/1165 [00:16<00:00, 45.09it/s] 99%|ββββββββββ| 1154/1165 [00:17<00:00, 43.19it/s] 99%|ββββββββββ| 1159/1165 [00:17<00:00, 44.21it/s]100%|ββββββββββ| 1164/1165 [00:17<00:00, 41.13it/s]100%|ββββββββββ| 1165/1165 [00:17<00:00, 67.46it/s]
0%| | 0/156 [00:00<?, ?it/s] 99%|ββββββββββ| 154/156 [00:00<00:00, 1533.81it/s]100%|ββββββββββ| 156/156 [00:00<00:00, 1518.02it/s]
2026-09-29 17:45:24 - INFO - Removed 67 isolated nodes.
edges["tf"].value_counts().head(10)tf
BCL11A 30
MEF2C 23
KLF4 11
POU2AF1 11
IRF8 8
IRF4 4
YBX3 3
EBF1 3
TCF4 3
CREB5 2
Name: count, dtype: int64The network consists of 115 links from 23 TFs to 84 genes, most of them from regulators of B cells, pDCs and monocytes such as BCL11A, MEF2C, POU2AF1, IRF8 and KLF4.
link_tf_to_gene only keeps TFs whose own gene is part of the network, so regulators that are not among the highly variable genes, such as LEF1, TCF7, CEBPA and SPI1, are missing although their motifs are enriched in the previous chapter.
Comparison with prior knowledgeΒΆ
To see how much of the network is supported by known regulation, we compare its TF-gene links with CollecTRI MΓΌller-Dott et al., 2023, the prior-knowledge network that we use in the RNA section.
collectri = ln.Artifact.get(key="conditions/gsea_pathway/collectri.parquet").load()
known = set(zip(collectri["source"], collectri["target"], strict=True))
edges["in_collectri"] = [
edge in known for edge in zip(edges["tf"], edges["target"], strict=True)
]
edges[edges["in_collectri"]]Only 4 of the 115 links are in CollecTRI, although CollecTRI contains all 23 TFs. Prior knowledge is incomplete and not specific to cell types, so the other links are not necessarily wrong, but they remain hypotheses until they are tested, for example by perturbing the TF.
QuizΒΆ
TheoryΒΆ
Networks from multiome dataΒΆ
ContributorsΒΆ
We gratefully acknowledge the contributions of:
AuthorsΒΆ
Ignacio Ibarra
ReviewersΒΆ
Lukas Heumos
Anna Schaar
- Spitz, F., & Furlong, E. E. M. (2012). Transcription factors: from enhancer binding to developmental control. Nat. Rev. Genet., 13(9), 613β626.
- Bravo GonzΓ‘lez-Blas, C., De Winter, S., Hulselmans, G., Hecker, N., Matetovici, I., Christiaens, V., Poovathingal, S., Wouters, J., Aibar, S., & Aerts, S. (2023). SCENIC+: single-cell multiomic inference of enhancers and gene regulatory networks. Nature Methods, 20(9), 1355β1367. 10.1038/s41592-023-01938-4
- Badia-i Mompel, P., Casals-Franch, R., Wessels, L., MΓΌller-Dott, S., Trimbour, R., Yang, Y., Ramirez Flores, R. O., & Saez-Rodriguez, J. (2024). Comparison and evaluation of methods to infer gene regulatory networks from multimodal single-cell data. bioRxiv. 10.1101/2024.12.20.629764
- Weirauch, M. T., Yang, A., Albu, M., Cote, A. G., Montenegro-Montero, A., Drewe, P., Najafabadi, H. S., Lambert, S. A., Mann, I., Cook, K., Zheng, H., Goity, A., van Bakel, H., Lozano, J.-C., Galli, M., Lewsey, M. G., Huang, E., Mukherjee, T., Chen, X., β¦ Hughes, T. R. (2014). Determination and Inference of Eukaryotic Transcription Factor Sequence Specificity. Cell, 158(6), 1431β1443. 10.1016/j.cell.2014.08.009
- Zhang, K., Zemke, N. R., Armand, E. J., & Ren, B. (2024). A fast, scalable and versatile tool for analysis of single-cell omics data. Nature Methods, 21(2), 217β227. 10.1038/s41592-023-02139-9
- MΓΌller-Dott, S., Tsirvouli, E., Vazquez, M., Ramirez Flores, R. O., Badia-i Mompel, P., Fallegger, R., TΓΌrei, D., LΓ¦greid, A., & Saez-Rodriguez, J. (2023). Expanding the coverage of regulons from high-confidence prior knowledge for accurate estimation of transcription factor activities. Nucleic Acids Research, 51(20), 10934β10949. 10.1093/nar/gkad841