Skip to main content

GLORB

Bayesian differential expression analysis for RNA-seq under global upregulation. Designed to be used in larger sample situations where global upregulation is expected and there are enough samples to safely learn it. This is potentially relevant to areas like large cancer studies that have been processed consistently but may have large differences between them.

Installation

pip install glorb-seq

You can also build from source by following the example below.

git clone https://github.com/rowancallahan/global_upreg_seq.git
cd global_upreg_seq
pip install -e .
pip install "jax[cpu]==0.9.1" numpyro==0.20.0

Requires Python ≥ 3.10. For GPU, use jax[cuda12] instead of jax[cpu].

Model Variants

# With size factor estimation (default) — for small datasets (< 35 samples per group)
# where you may have large bias in sample handling or processing between conditions
results, losses, svi = jax_run_pyro(counts.T, labels, key, use_size_factor_model=True)

# Without size factors — recommended with 35–50+ samples per group, makes training
# more stable with a more flexible representation of the data
results, losses, svi = jax_run_pyro(counts.T, labels, key, use_size_factor_model=False)

Differences from DESeq2 and Other Methods

  • Direct posterior inference: Instead of p-values from a frequentist test, GLORB returns plesser — the posterior probability that a gene's absolute log fold change is small (below 1). This means you can make positive claims about non-DE genes (high plesser), not just fail to reject the null.
  • Robust to global upregulation: When a large fraction of genes are DE in the same direction, median-of-ratios normalization (DESeq2, edgeR) systematically underestimates fold changes. GlORB's spike-and-slab prior separates DE from non-DE genes during inference, avoiding this bias.

Quick Start

import numpy as np
import pandas as pd
import jax
from global_upreg_seq import jax_run_pyro

counts = pd.read_csv("counts.csv", index_col=0)  # genes × samples
labels = np.array([0, 0, 0, 1, 1, 1])             # 0=control, 1=treatment

key = jax.random.PRNGKey(0)
results, losses, svi = jax_run_pyro(
    counts.values.T,  # [N, P] — samples × genes
    labels, key, iterations=3000, use_size_factor_model=True,
)

de_results = pd.DataFrame({
    "gene": counts.index,
    "log2fc": results["log2fc"],
    "plesser": results["plesser"],
})
de_results["significant"] = (de_results["plesser"] < 0.05) & (de_results["log2fc"].abs() > 1)

Interpreting Results

  • log2fc — posterior mean log₂ fold change per gene
  • plesser — P(|log FC| < ln 2), the probability the effect is small. Lower = more significant. Call DE at plesser < 0.05

Finding Stably Expressed Genes

plesser directly quantifies the probability a gene's fold change is small.

de_results["stable"] = de_results["plesser"] > 0.95  # >95% probability of no change

Design Matrices

🚧 !! Multi-factor design matrices (F > 1) are not fully supported yet !! The model architecture handles arbitrary [N, F] matrices, but the data-driven initialization assumes binary labels and will produce incorrect starting values for F > 1. Binary two-group comparisons (F = 1) work correctly. See Roadmap for details.

GLORB accepts arbitrary design matrices [N, F]. A 1D label array is reshaped to [N, 1] automatically. For categorical variables, use one-hot encoding with K−1 columns (drop one category as the reference). Libraries like formulaic or patsy handle this automatically with formula syntax.

from formulaic import model_matrix  # pip install formulaic

metadata = pd.DataFrame({
    "condition": ["A", "A", "A", "B", "B", "B", "C", "C", "C"],
    "batch":     ["x", "x", "y", "x", "y", "y", "x", "y", "y"],
})

# Drop intercept — GlobSeq has its own (log_mu0)
X = np.array(model_matrix("~ condition + batch", metadata))[:, 1:]

results, losses, svi = jax_run_pyro(counts.values.T, X, key, iterations=3000)
# results["log2fc"] is [F, P], results["plesser"] is [F, P]

Pairwise Comparisons from Multi-Category Designs

With 4 categories (A, B, C, D) and A as reference, the design matrix gives 3 factors: B−A, C−A, D−A. Results vs the reference come directly from the output. For non-reference pairwise comparisons (e.g. B vs C), subtract the posterior parameters — variances add under the independent normal variational guide:

import jax.numpy as jnp
import numpyro.distributions as dist
from itertools import combinations

def pairwise_plesser(svi, factor_names, cutoff=jnp.log(2.0)):
    """Compute plesser for all pairwise comparisons from a multi-category fit.

    Args:
        svi: SVIRunResult from jax_run_pyro
        factor_names: list of factor names matching design matrix columns
        cutoff: significance cutoff in natural log scale (default ln(2))

    Returns:
        dict of {("B", "C"): plesser_array, ...} for all pairs
    """
    lfc_loc = svi.params["log_fc_auto_loc"]      # [F, P]
    lfc_scale = svi.params["log_fc_auto_scale"]  # [F, P]
    results = {}
    for i, j in combinations(range(len(factor_names)), 2):
        diff_loc = lfc_loc[i] - lfc_loc[j]
        diff_scale = jnp.sqrt(lfc_scale[i]**2 + lfc_scale[j]**2)
        results[(factor_names[i], factor_names[j])] = dist.Normal(
            jnp.abs(diff_loc), diff_scale
        ).cdf(cutoff)
    return results

# Usage
pw = pairwise_plesser(svi, ["B", "C", "D"])
for pair, plesser in pw.items():
    print(f"{pair[0]} vs {pair[1]}: {(plesser < 0.05).sum()} DE genes")

Replicates

Different random keys give independent inference runs:

for rep in range(5):
    key = jax.random.fold_in(jax.random.PRNGKey(0), rep)
    results, losses, svi = jax_run_pyro(counts.T, labels, key)

Simulated Data

from global_upreg_seq import jax_generate_simulated_data

counts, labels, (log_fc_true, size_factors, base_means) = jax_generate_simulated_data(
    group_size=10, gene_size=30000, median_log_upreg=1.5,
    non_de_fraction=0.25, seed=42,
)

Platform

export JAX_PLATFORMS=cpu   # laptops
export JAX_PLATFORMS=cuda  # GPU

Roadmap

  • Multi-factor design matrix initialization: The model supports arbitrary [N, F] design matrices, but the data-driven initialization (jax_prepare_norm_mode, jax_prepare_initialization) currently assumes binary labels. For now, multi-factor designs will use a collapsed binary init (reference category vs everything else).
  • Mixed categorical + continuous covariates: Requires a factor_types parameter so the init knows which columns are categorical (used to find the reference group) and which are continuous (ignored during init).
  • numpyro > 0.20.0 compatibility: numpyro.optim and numpyro.set_platform() were removed in newer numpyro. Currently pinned to numpyro==0.20.0.
  • CAVI inference: Coordinate ascent variational inference as an alternative to SVI for faster convergence.

Citation

  @article{callahan2026glorb,
  title={GLORB: Robust Bayesian inference for differential expression under global expression shifts},
  author={Callahan, Rowan L. and Coleman, Stephen D. and Ngo, Thuy T. M.},
  year={2026},
  month=sep,
  journal={bioRxiv},
  note={Preprint},
  doi={10.64898/2026.08.28.747928},
  url={https://doi.org/10.64898/2026.08.28.747928}
  }

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

glorb_seq-1.1.1.tar.gz (28.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

glorb_seq-1.1.1-py3-none-any.whl (26.4 kB view details)

Uploaded Python 3

File details

Details for the file glorb_seq-1.1.1.tar.gz.

File metadata

  • Download URL: glorb_seq-1.1.1.tar.gz
  • Upload date:
  • Size: 28.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for glorb_seq-1.1.1.tar.gz
Algorithm Hash digest
SHA256 895d184672f93ccd0777354b8a0170b8d3a3966744d2b0a08746e1b6a3e9ca1f
MD5 b87a751634288a06c2979857168894dd
BLAKE2b-256 5ff7356e62120d2ee2096da6e9cc9556c36cffb062bd78b0df766f49295592a4

See more details on using hashes here.

Provenance

The following attestation bundles were made for glorb_seq-1.1.1.tar.gz:

Publisher: python-publish.yml on rowancallahan/GLORB-seq

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file glorb_seq-1.1.1-py3-none-any.whl.

File metadata

  • Download URL: glorb_seq-1.1.1-py3-none-any.whl
  • Upload date:
  • Size: 26.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for glorb_seq-1.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 04facaa7dd021ccf8eaa436ebfec83ab18033011835baffdabc13d35b229f38c
MD5 7b61739f004d001c75ec1ef8c40a3436
BLAKE2b-256 d57d5d5384314c80c5de5733f8fc330a761f479dc6e4ecefdbc5cedefdaf8c18

See more details on using hashes here.

Provenance

The following attestation bundles were made for glorb_seq-1.1.1-py3-none-any.whl:

Publisher: python-publish.yml on rowancallahan/GLORB-seq

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

1.1.1 This release

2 files

1.1.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page