IVAX Carrier Effect in scRNA-seq GSE287619
By Ju Lin · Published August 26, 2026
Reproduces nanoparticle carrier amplification claims from a 2026 immunology study by reanalyzing GSE287619 scRNA-seq data with quasi-Poisson GLM on per-mouse cell composition.
- single-cell-rna
- immunology
- composition-analysis
- glm
- vaccine
Inside this notebook
# Reproducing the IVAX carrier claim in a real scRNA-seq dataset **Target abstract.** Basirin CS, Li Y, Felgner J, Ozer E, Chiang J-L, Davies DH, Felgner PL, Liang L. *"Nanoparticle delivery amplifies the adjuvant effects of TLR agonists for cancer vaccines"* (2310294). **J Immunol** 215(Supplement_1):vkag141.1733 — AAI IMMUNOLOGY2026, published online 2026‑07‑28. DOI [10.1093/jimmun/vkag141.1733](https://doi.org/10.1093/jimmun/vkag141.1733) · OpenAlex `W7171492144` · UC Irvine Vaccine R&D Center · NIAID award 1U19AI181968‑01. The abstract's claim, verbatim from its Methods/Results: three carriers — nanoemulsion (**IVAX‑1**), cationic LNP (**IVAX‑5**), ionizable LNP (**IVAX‑6**) — each encapsulating a **CpG (TLR9) + MPLA (TLR4)** combination adjuvant with **OVA** antigen; readouts were physicochemical characterization, **RAW‑Blue NF‑κB** reporter activation, humoral/cellular immunogenicity, reactogenicity, and antitumor efficacy in **B16F10‑OVA** melanoma. ## Why this notebook is a *reanalysis*, not a literal reproduction This is a **conference abstract**: no figures, no methods detail, no statistics, and **no deposited data**. It is entirely wet-lab and in vivo (mouse melanoma). Nothing in it can be literally recomputed. Claiming otherwise would be fabrication, so this notebook does the strongest honest thing available: > **It tests the abstract's central mechanistic claim — that the nanoparticle carrier *amplifies* TLR-agonist > adjuvant effects — against real, public,…
"""
Section 1 — Environment.
scanpy/anndata/h5py/lifelines are NOT in this image's manifest, so they must be installed.
Four environment quirks handled here:
1. PYTHONPATH ships as '/pkg/:/root/', and /root is NOT readable by this user. pip scans every
sys.path entry when it builds its session, so it dies with PermissionError: '/root' before
resolving anything. Fix = sanitize PYTHONPATH for the pip subprocess.
2. scanpy pulls numba/umap, which can drag numpy/pandas/scipy along. A constraints file pins the
preinstalled scientific stack so the CUDA torch build never gets its numpy swapped underneath it.
3. The matplotlib backend is pinned to the notebook-inline backend ONCE, here at the top. Do NOT call
matplotlib.use("Agg") anywhere downstream: Agg is a non-interactive file-only backend, so plt.show()
becomes a silent no-op and every figure vanishes from the cell output while savefig still writes the
PNG to disk. That is exactly the failure mode this notebook hit on its first pass.
4. The plotly renderer is pinned to 'plotly_mimetype'. The auto-detected default is
'plotly_mimetype+notebook', which ALSO injects a ~4.5 MB copy of plotly.js into every single
figure's cell output — the notebook balloons by tens of MB for zero benefit, because the client
…sanitized PYTHONPATH: '/pkg/'
"""
Section 2 — Acquire GSE287619 from NCBI GEO.
Two downloads:
* the supplementary .h5ad (15.8 GiB) -> the actual expression matrix
* the series matrix .txt.gz (~KB) -> authoritative GSM -> treatment-group mapping
IMPORTANT placement choice: the 15.8 GiB h5ad goes to /tmp, NOT /home/user. Files under /home/user are
auto-uploaded to the outputs bucket after every cell, and re-uploading a 16 GB intermediate on each run
would be enormous and useless. Only small derived artifacts (tables, figures, report) go to /home/user.
"""
import os, time, gzip, shutil, urllib.request
from pathlib import Path
RAW = Path("/tmp/ivax_data"); RAW.mkdir(parents=True, exist_ok=True)
BASE = "https://ftp.ncbi.nlm.nih.gov/geo/series/GSE287nnn/GSE287619"
H5AD_URL = f"{BASE}/suppl/GSE287619_FullExpFiltered_HigherGeneQC.h5ad"
…series matrix:
"""
Section 3 — Inspect provenance and schema WITHOUT loading the matrix.
(a) Parse the GEO series matrix -> authoritative GSM -> treatment mapping (ground truth for group labels).
(b) Walk the .h5ad with raw h5py so nothing large is read into RAM, and report how X is stored
(dense vs CSR) since that dictates the whole memory strategy.
(c) Dump obs/var schema + categorical levels so we discover the real column names instead of guessing.
"""
import gzip, h5py, numpy as np, pandas as pd
# ---------------------------------------------------------------- (a) GEO series matrix
print("=" * 100)
print("(a) GEO series matrix — authoritative sample metadata")
print("=" * 100)
with gzip.open(SM, "rt", errors="replace") as f:
sm_lines = [ln.rstrip("\n") for ln in f]
keep = ("!Series_title", "!Series_geo_accession", "!Series_summary", "!Series_overall_design",
…====================================================================================================
(a) GEO series matrix — authoritative sample metadata
====================================================================================================
!Series_title:
Combination adjuvant improves influenza virus immunity by downregulation of immune homeostasis genes in lymphocytes
!Series_geo_accession (n=1):
GSE287619
!Series_summary:
Adjuvants play a central role in enhancing th…"""
Section 4 — Load an analysis-ready object and lock in the experimental design.
The file is 15.8 GiB almost entirely because of layers['scaled'] = a DENSE 146532 x 21822 float32 block
(11.9 GiB). We never need it (scaling is only for PCA, which is already precomputed in obsm), so we read
elements selectively via h5py and skip it. What we keep:
X log1p-normalized CSR (uns['log1p'] present, values are non-integral) -> ~2 GiB
layers['counts'] raw integer CSR -> ~2 GiB
obs / var / obsm (X_umap, X_pca_harmony) authored by the original investigators
DESIGN — the reason this dataset can test the 2026 abstract at all:
the four antigen-containing arms are a complete 2x2 factorial.
agonists (CpG+MPLA) - +
carrier (AddaVax) - H5/PBS H5/CpG+MPLA
carrier (AddaVax) + H5/AddaVax H5/IVAX
"""
import h5py, numpy as np, pandas as pd, anndata as ad, scanpy as sc, time
…loaded slim cache in 15.4s
AnnData object with n_obs × n_vars = 146532 × 21822
obs: 'Batch', 'Exp', 'Condition', 'n_genes', 'n_genes_by_counts', 'log1p_n_genes_by_counts', 'total_counts', 'log1p_total_counts', 'pct_counts_in_top_50_genes', 'pct_counts_in_top_100_genes', 'pct_counts_in_top_200_genes', 'pct_counts_in_top_500_genes', 'total_counts_mito', 'log1p_total_counts_mito', 'pct_counts_mito', 'total_counts_Rps', 'log1p_total_counts_Rps', 'pct_counts_Rps', 'leiden0.7', 'leiden0.3', 'Condi…"""
Section 5 — QC filter, annotation hierarchy, and per-mouse composition.
Doublets are removed before ANY composition claim: the object carries both a scored `Doublet` flag and an
explicit 'B/T cell doublets' cluster, and leaving either in place would inflate exactly the kind of
plasmablast/B-cell fractions R1 is about.
Composition is computed PER MOUSE (n=3 per arm), never pooled per arm. Pooling cells across mice would
treat ~10,000 cells as ~10,000 independent observations and manufacture astronomically small p-values;
the true replicate here is the mouse.
NOTE: no matplotlib backend switch here. The inline backend is set once in Section 1; calling
matplotlib.use("Agg") at this point would silence plt.show() for every downstream cell.
"""
import numpy as np, pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
…doublet-scored cells : 4,551 (3.11%) 'B/T cell doublets' cluster : 3,972 (2.71%) union removed : 5,372
## 6. R1 + R5 — plasmablast expansion and the *amplification* test Two statistical decisions matter more than anything else here, so they are stated explicitly: **The replicate is the mouse, n = 3 per arm.** Composition is modelled from per-mouse cell counts with a `log(total cells)` offset under a **quasi-Poisson** GLM (Poisson mean–variance with a free dispersion scale estimated by Pearson χ²). This is the standard small-replicate compositional approach: it propagates the real between-mouse overdispersion instead of pretending each of ~141,000 cells is independent. Treating cells as replicates here would yield p-values around 1e-300 that mean nothing. **"Amplifies" is an interaction claim, so it is tested as one.** On the 4-arm factorial: $$\log \mathbb{E}[\text{count}] = \beta_0 + \beta_c\,\text{carrier} + \beta_a\,\text{agonist} + \beta_{ca}\,(\text{carrier}\times\text{agonist}) + \log N$$ - $\beta_c$ — carrier alone (AddaVax vs H5) - $\beta_a$ — free agonists alone (CpG+MPLA vs H5) - $\beta_{ca}$ — **the amplification term.** $\exp(\beta_{ca}) > 1$ means the combination exceeds the product of the single effects (super-additive / synergistic); $= 1$ means multiplicatively additive; $< 1$ means the combination delivers *less* than the two components predict. We also report the direct **IVAX‑1 vs CpG+MPLA** contrast, which is the cleanest available analogue of the abstract's "nanoparticle delivery amplifies TLR agonists": same agonists, ± carrier. > **Power cavea…
This is a preview. Open the live notebook to see all 30 cells with their charts and full outputs, or fork it into your own Clusy workspace.