Skip to content

gtars.lola

Python bindings for gtars-lola — LOLA (Locus Overlap Analysis) enrichment testing. Given a user set of regions, a universe (background), and a region database, computes Fisher's exact test enrichment of the user set against every database region set.

See the Rust gtars-lola page for the full statistical reference (Fisher's exact test, CMLE odds ratio, contingency table semantics, R LOLA compatibility notes). This page is a Python-focused reference.

from gtars.models import RegionSet
from gtars.lola import RegionDB, run_lola
# 1. Load the region database from a LOLA-format folder
db = RegionDB.from_folder(
"LOLACore/hg38",
collections=None, # None = all; or pass a list of collection names
limit=None, # None = all files; or pass a per-collection cap
)
# 2. Prepare user sets and universe as lists of (chr, start, end) tuples
peaks = RegionSet("peaks.bed")
universe = RegionSet("universe.bed")
def to_tuples(rs):
return [(r.chr, r.start, r.end) for r in rs]
user_sets = [to_tuples(peaks)] # list of user sets; each is a list of tuples
universe_tuples = to_tuples(universe)
# 3. Run the enrichment
results = run_lola(
user_sets,
universe_tuples,
db,
min_overlap=1,
direction="enrichment", # or "depletion"
)
# results is a column-oriented dict — DataFrame-friendly
import pandas as pd
df = pd.DataFrame(results)
df = df.sort_values("pValueLog", ascending=False)
print(df.head(20))

The 4-step workflow mirrors R LOLA: load database → convert inputs → run enrichment → pandas-ify.

Wraps a gtars-lola RegionDB — an IGD overlap index plus original region sets plus per-file annotations, loaded from the standard LOLA directory layout:

db_path/
├── collection1/
│ ├── collection.txt # collector, date, source, description (TSV/CSV)
│ ├── index.txt # per-file annotations (TSV/CSV)
│ └── regions/
│ ├── file1.bed
│ └── file2.bed
├── collection2/
│ └── …
RegionDB.from_folder(db_path, collections=None, limit=None) -> RegionDB
  • db_path — path to the LOLA database root.
  • collections — optional list of collection names to include (others are skipped).
  • limit — optional per-collection file cap (useful for smoke tests).

collection.txt and index.txt are auto-detected TSV or CSV. Recognized index.txt columns: filename, cellType, description, tissue, dataSource, antibody, treatment. Unknown columns are ignored; missing optional columns become None.

Build a database from a list of BED file paths without the full LOLA layout:

db = RegionDB.from_bed_files(
["file1.bed", "file2.bed", "file3.bed"],
filenames=None, # optional; defaults to basenames
)

Useful for ad-hoc analyses where you have a list of peak files but don't want to set up a collection directory.

db.num_region_sets # int — total files in the database
db.list_region_sets() # list[str] — all filenames
db.list_region_sets(collections=["enc3"]) # filtered
# Extract region sets as a gtars.models.RegionSetList (names populated from filenames)
rsl = db.get_region_sets() # all sets
rsl = db.get_region_sets(indices=[0, 5, 12])
# Annotations as lists of dicts
db.region_anno # per-file dicts with filename/cellType/description/tissue/...
db.collection_anno # per-collection dicts with collectionname/collector/date/source/description

get_region_sets() returns a gtars.models.RegionSetList, which has names populated from the database filenames. This is the one path in Python where RegionSetList.names is non-None — normal RegionSetList(...) construction can't set names.

run_lola(
user_sets: list[list[tuple[str, int, int]]],
universe: list[tuple[str, int, int]],
region_db: RegionDB,
min_overlap: int = 1,
direction: str = "enrichment",
) -> dict

Parameters:

  • user_sets — a list of user sets, where each user set is a list of (chr, start, end) tuples. Pass one-element list [peaks_tuples] for a single-user-set analysis.
  • universe — a single list of (chr, start, end) tuples representing the background.
  • region_db — a RegionDB.
  • min_overlap — minimum bp overlap to count as overlapping (default 1).
  • direction — "enrichment" (default, P(X ≥ a), alternative "greater") or "depletion" (P(X ≤ a), alternative "less"). The strings "greater" / "less" are accepted as aliases.

Returns a column-oriented dict with these parallel lists (one entry per (user_set, db_set) pair):

keytypemeaning
userSetlist[int]0-based user-set index
dbSetlist[int]0-based db-set index
collectionlist[str | None]from index.txt
pValueLoglist[float]-log10(p) from Fisher's exact test, capped at ~322
oddsRatiolist[float]CMLE odds ratio (matches R fisher.test()$estimate)
supportlist[int]overlap count between user set and db set — the contingency a
rnkPV, rnkOR, rnkSuplist[int]1-based per-metric ranks within the user set
maxRnklist[int]max of the three ranks
meanRnklist[float]mean of the three ranks
b, c, dlist[int]signed contingency values (can be negative if user set extends outside universe)
qValuelist[float | None]BH-adjusted p-value (applied automatically inside run_lola)
description, cellType, tissue, antibody, treatment, dataSourcelist[str | None]from index.txt
filenamelist[str]db set file name
sizelist[int]number of regions in the db set

The Python run_lola already calls annotate_results + apply_fdr_correction internally, so qValue and the annotation columns come back populated — you don't need to post-process.

Rows are sorted by descending pValueLog, then ascending meanRnk (matching R LOLA output order).

LOLA results are only meaningful when your universe contains your user sets. Three helpers for checking and shaping the universe:

from gtars.lola import check_universe
report = check_universe(
user_sets, # list[list[tuple[str, int, int]]]
universe_tuples, # list[tuple[str, int, int]]
)
# report is a dict with parallel lists:
report["userSet"] # list[int]
report["totalRegions"] # list[int]
report["regionsInUniverse"] # list[int]
report["coverage"] # list[float] — 0.0 to 1.0
report["manyToMany"] # list[int] — number of user regions overlapping >1 universe region
report["warnings"] # list[str] — free-form warning messages

Warnings fire when coverage is under 50% (severe) or under 90% (moderate), and when any user region overlaps more than one universe region.

Rewrite each user set in terms of universe regions — for every user region, find the universe regions it overlaps and emit those as the new user set. Eliminates many-to-many mapping artifacts. This is the Python equivalent of R LOLA's redefineUserSets().

from gtars.lola import redefine_user_sets
redefined = redefine_user_sets(user_sets, universe_tuples)
# redefined is list[list[tuple[str, int, int]]] — same shape as input, but rewritten
# Now run enrichment on the redefined version
results = run_lola(redefined, universe_tuples, db)

For differential enrichment analysis — build a universe that is exactly the union of all user sets, disjoined into non-overlapping pieces. This is R LOLA's disjoin(unlist(userSets)).

from gtars.lola import build_restricted_universe
restricted = build_restricted_universe(user_sets)
# restricted: list[tuple[str, int, int]]
# Use it as the universe in a differential run
results = run_lola(user_sets, restricted, db)
import pandas as pd
from gtars.models import RegionSet
from gtars.lola import RegionDB, run_lola, check_universe, redefine_user_sets
# Convert a RegionSet to the tuple form lola expects
def to_tuples(rs):
return [(r.chr, r.start, r.end) for r in rs]
# Load inputs
db = RegionDB.from_folder("LOLACore/hg38")
peaks_a = to_tuples(RegionSet("condition_a.bed"))
peaks_b = to_tuples(RegionSet("condition_b.bed"))
universe = to_tuples(RegionSet("universe.bed"))
user_sets = [peaks_a, peaks_b]
# Universe sanity check
report = check_universe(user_sets, universe)
for us, cov, mm in zip(
report["userSet"], report["coverage"], report["manyToMany"]
):
print(f"user set {us}: {cov:.1%} coverage, {mm} many-to-many")
for w in report["warnings"]:
print(f" ⚠ {w}")
# Optional: eliminate many-to-many artifacts
user_sets = redefine_user_sets(user_sets, universe)
# Run enrichment
results = run_lola(user_sets, universe, db, direction="enrichment")
df = pd.DataFrame(results)
print(df.shape) # (n_user_sets * n_db_sets, 23)
# Top 10 per user set
for us in df["userSet"].unique():
top = (
df[df["userSet"] == us]
.sort_values("pValueLog", ascending=False)
.head(10)
)
print(f"\n=== User set {us} — top 10 ===")
print(top[["filename", "cellType", "pValueLog", "oddsRatio", "support", "qValue"]])
  • gtars.models — RegionSet, RegionSetList, and the types returned by RegionDB.get_region_sets().
  • gtars-lola — full Rust API reference, including contingency table math, p-value computation via hypergeometric survival function, CMLE odds ratio, and the R LOLA TSV output format.