Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Pseudobulk and differential expression (DE) primer

To compare gene expression changes across conditions, we need independent biological samples in each group. Pseudobulking works for single-cell RNA sequencing data because summing raw counts within each donor produces donor-level count data that mirrors bulk RNA-seq count data. Testing for differential expression tells us which genes change between conditions, pointing to the programs driving disease or response. Without it, we can describe what genes are present and potentially driving an effect, but not what is different about them across the conditions.

Dataset and study design

The data come from the CZ CELLxGENE collection Single-cell RNA-Seq analysis reveals cell subsets and gene signatures associated with rheumatoid arthritis disease activity, published with Binvignat et al.. The study contains PBMCs from 18 people with rheumatoid arthritis (RA) and 18 matched controls processed across three batches. If you remember correctly, this is the same dataset used in the Compare biological conditions across donors

Here each sample is a donor. This tutorial simply goes through the motions of preparing our data for true DE analysis, by summing the raw counts from γδ T cells into one column per donor, keeping the matched study design, and thus exporting the result for a method such as edgeR or DESeq2. CyteArc performs the aggregation and export. It does not fit the differential expression model on this page.

Prepare the study data

To begin, simply download the dataset and inspect it to derive the existing information.

'binvignat_ra_pbmc.h5ad'

Started: 2026-10-11T18:54:53.353336Z | Wall time: 2.546 s

Download the file if it is not already present.

{'file': 'binvignat_ra_pbmc.h5ad', 'bytes': 259685464}

Started: 2026-10-11T18:54:55.905757Z | Wall time: 18.889 s

Inspect the available matrices before converting the file.

{'matrix': 'raw/X', 'encoding': 'csr', 'integer-like': True, 'shape': (108717, 21648)}

Started: 2026-10-11T18:55:14.799911Z | Wall time: 1.414 s

After inspection, we see the expected 108,717 cells by 21,648 features (genes). We can then proceed by converting the .h5ad file to its corresponding zarr format.

'binvignat_ra_pbmc.zarr'

Started: 2026-10-11T18:55:16.217419Z | Wall time: 30.401 s

Open the converted count store. min_features_per_cell=0 drops only cells without detected features (none here), so every curated cell stays active.

DataStore has 108717 (108717) cells with 1 assays: RNA Cell metadata: 'I', 'ids', 'names', 'G2M_score', 'Lane', 'MY_CCP_Status', 'MY_MTX', 'MY_RF_status', 'MY_bDMARD', 'MY_cdai', 'MY_crp', 'MY_das28crp4', 'MY_das28esr4', 'MY_esr', 'MY_md_global', 'MY_pt_global', 'MY_sjc', 'MY_tjc', 'RNA_nCounts', 'RNA_nFeatures', 'RNA_percentMito', 'RNA_percentRibo', 'S_score', 'activity_python_binary_crp', 'activity_python_binary_esr', 'activity_python_crp', 'activity_python_esr', 'assay', 'assay_ontology_term_id', 'batch', 'cell_cycle_diff', 'cell_type', 'cell_type_ontology_term_id', 'dead_cells', 'demux_N_SNP', 'demux_PRB_DBL', 'demux_RD_PASS', 'demux_RD_TOTL', 'demux_RD_UNIQ', 'demux_doublet_call', 'development_stage', 'development_stage_ontology_term_id', 'disease', 'disease_ontology_term_id', 'donor_id', 'fine_annot', 'is_primary_data', 'leiden_r3.0', 'live_cells', 'nCount_RNA', 'nFeature_RNA', 'n_counts', 'n_genes', 'n_genes_by_counts', 'norm_library_size', 'observation_joinid', 'pair_index_CW', 'pct_alive', 'pct_counts_hb', 'pct_counts_mt', 'pct_counts_ribo', 'percent_mt', 'phase', 'rough_annot', 'scrub_doublets', 'self_reported_ethnicity', 'self_reported_ethnicity_ontology_term_id', 'sex', 'sex_ontology_term_id', 'suspension_type', 'tissue', 'tissue_ontology_term_id', 'tissue_type', 'total_counts', 'total_counts_hb', 'total_counts_mt', 'total_counts_ribo' RNA assay has 21648 features and following metadata: 'I', 'ids', 'names', 'dropOuts', 'feature_biotype', 'feature_length', 'feature_reference', 'feature_type', 'hb', 'mean_counts', 'mt', 'nCells', 'n_cells', 'n_cells_by_counts', 'pct_dropout_by_counts', 'ribo', 'total_counts'

Started: 2026-10-11T18:55:46.621154Z | Wall time: 3.488 s

Mount a separate target for the analysis.

DataStore has 108717 (108717) cells with 1 assays: RNA Cell metadata: 'I', 'ids', 'names', 'G2M_score', 'Lane', 'MY_CCP_Status', 'MY_MTX', 'MY_RF_status', 'MY_bDMARD', 'MY_cdai', 'MY_crp', 'MY_das28crp4', 'MY_das28esr4', 'MY_esr', 'MY_md_global', 'MY_pt_global', 'MY_sjc', 'MY_tjc', 'RNA_nCounts', 'RNA_nFeatures', 'RNA_percentMito', 'RNA_percentRibo', 'S_score', 'activity_python_binary_crp', 'activity_python_binary_esr', 'activity_python_crp', 'activity_python_esr', 'assay', 'assay_ontology_term_id', 'batch', 'cell_cycle_diff', 'cell_type', 'cell_type_ontology_term_id', 'dead_cells', 'demux_N_SNP', 'demux_PRB_DBL', 'demux_RD_PASS', 'demux_RD_TOTL', 'demux_RD_UNIQ', 'demux_doublet_call', 'development_stage', 'development_stage_ontology_term_id', 'disease', 'disease_ontology_term_id', 'donor_id', 'fine_annot', 'is_primary_data', 'leiden_r3.0', 'live_cells', 'nCount_RNA', 'nFeature_RNA', 'n_counts', 'n_genes', 'n_genes_by_counts', 'norm_library_size', 'observation_joinid', 'pair_index_CW', 'pct_alive', 'pct_counts_hb', 'pct_counts_mt', 'pct_counts_ribo', 'percent_mt', 'phase', 'rough_annot', 'scrub_doublets', 'self_reported_ethnicity', 'self_reported_ethnicity_ontology_term_id', 'sex', 'sex_ontology_term_id', 'suspension_type', 'tissue', 'tissue_ontology_term_id', 'tissue_type', 'total_counts', 'total_counts_hb', 'total_counts_mt', 'total_counts_ribo' RNA assay has 21648 features and following metadata: 'I', 'ids', 'names', 'dropOuts', 'feature_biotype', 'feature_length', 'feature_reference', 'feature_type', 'hb', 'mean_counts', 'mt', 'nCells', 'n_cells', 'n_cells_by_counts', 'pct_dropout_by_counts', 'ribo', 'total_counts'

Started: 2026-10-11T18:55:50.111374Z | Wall time: 2.073 s

Select the γδ T-cells

For this matched study, keep only cells with a finite matched-pair identifier so every selected cell can be assigned to the donor design used below.

donor_iddiseasebatchpair_index_CWfine_annot
0RA09_1_C_Mrheumatoid arthritis29.0Classical Monocytes
1RA09_1_C_Mrheumatoid arthritis29.0IL1b-Monocytes
2RA08_1_C_Mrheumatoid arthritis18.0Myeloid DCs
3Control03_1_C_Fnormal13.0Myeloid DCs
4Control10_1_C_Fnormal210.0CD4 T central memory

Started: 2026-10-11T18:55:52.187253Z | Wall time: 0.106 s

Select the matched γδ T cells and freeze their row selection.

selected cells 1386 represented donors 36 dtype: int64

Started: 2026-10-11T18:55:52.295996Z | Wall time: 0.080 s

This selection freezes the chosen cells so that the pseudobulk counts and donor metadata describe the same population.

“Pseudobulk” (sum raw counts by biological donor)

Pseudobulking is simply the process of summing the raw counts by biological donor, thus, we use the aggr_type="sum" argument to take the raw assay counts and produce one column of counts per donor_id.

Control01_1_A_MControl03_1_C_FControl04_1_C_FControl05_1_C_MControl06_1_C_MControl07_1_C_F
LINC01409001000
LINC00115001000
SAMD11100000
NOC2L010000
KLHL17000000

Started: 2026-10-11T18:55:52.378463Z | Wall time: 1.780 s

The resulting pseudobulk leaves us with 13,547 expressed features and 36 donor columns. Our features dropped from > 20,000 to ~13000 as this step automatically removes features with zero counts across the selected γδ T cells; the same applies for whatever your selection of cells is.

Build and verify the donor design

Each donor must have exactly one disease, matched-pair value, and batch within this exact selected population. The design is then aligned to the count-matrix columns before we export.

diseasepair_index_CWbatch
donor_id
RA10_1_C_F111
Control15_1_C_F111
RA09_1_C_M111
RA20_1_C_F111
Control23_1_C_F111
RA06_1_C_M111
Control09_1_C_M111
RA19_1_A_F111
Control16_1_C_F111
RA15_1_C_F111
RA01_1_A_M111
RA08_1_C_M111
Control08_1_C_M111
Control10_1_C_F111
RA17_1_C_F111
Control19_1_C_F111
RA07_1_C_F111
Control04_1_C_F111
RA16_1_C_M111
RA18_1_C_F111
Control22_1_H_F111
RA21_1_H_F111
Control21_1_C_F111
RA05_1_C_M111
RA03_1_C_F111
Control07_1_C_F111
Control01_1_A_M111
RA23_1_C_F111
Control20_1_A_F111
Control17_1_C_M111
Control03_1_C_F111
RA22_1_C_F111
Control06_1_C_M111
Control05_1_C_M111
Control18_1_C_F111
RA04_1_C_F111

Started: 2026-10-11T18:55:54.161357Z | Wall time: 0.020 s

Align one metadata row per donor with the count columns.

diseasepair_index_CWbatch
Control01_1_A_Mnormal1.01
Control03_1_C_Fnormal3.01
Control04_1_C_Fnormal4.01
Control05_1_C_Mnormal5.01
Control06_1_C_Mnormal6.01

Started: 2026-10-11T18:55:54.184217Z | Wall time: 0.016 s

Check the donor counts and matched pairs.

diseasenormalrheumatoid arthritis
batch
177
243
378

Started: 2026-10-11T18:55:54.203920Z | Wall time: 0.020 s

The 36 columns are 36 biological replicates, arranged as 18 RA-control pairs. The batch describes the cells that actually contributed to each donor column.

Export raw counts and design metadata

With our counts and our metadata table, simply export the 2 tables for DE analysis offline. These will remain available after the notebook closes. The donor metadata has already been aligned to the count columns.

pseudobulk_exports/yd_t_cell_raw_counts.csv
pseudobulk_exports/yd_t_cell_donor_design.csv

Started: 2026-10-11T18:55:54.226658Z | Wall time: 0.101 s

Use bulk as the raw feature-by-donor count matrix. The external model must use donor-level replication and account for the study design. The linked publication at the top of this notebook allows you find the exact parameters the authors used, and replicate the results yourself.

Optional: explore a reported γδ T-cell panel

Library-normalized values are useful for a compact descriptive view before modeling. These values show each donor’s expression on a common per-million scale, so differences between conditions can be eyeballed before any model is fit. This rescales each donor by its total counts; it does not equalize the number of cells or biological replicates between groups. Doing this only describes the data, and doesn’t test it for any changes. The figure below converts the donor pseudobulks to log2 counts per million (CPM) only for visualization.

Start with one gene, IFNG, so each line can show one matched pair.

diseasenormalrheumatoid arthritis
pair_index_CW
1.00.0000005.277827
3.00.0000000.000000
4.07.1439410.000000
5.05.4166230.000000
6.00.0000005.930709

Started: 2026-10-11T18:55:54.330224Z | Wall time: 0.028 s

Plot the matched control and RA values.

Paired lines connect control and rheumatoid-arthritis IFNG values in gamma-delta T-cell pseudobulks on a log2(CPM + 1) scale. Some pairs increase and others decrease.

Started: 2026-10-11T18:55:54.361091Z | Wall time: 0.252 s

This panel shows donor heterogeneity and paired direction, but it does not estimate dispersion, adjust for batch, fit the matched design, or test a hypothesis. It must not be reported as a differential expression result.

Pseudo-replicates are not biological replicates

compare.compute_bulk(..., pseudo_reps=2) randomly divides cells within a donor. Those partitions can support descriptive stability checks, but they come from the same person and do not increase the biological sample size. This tutorial leaves pseudo_reps at its default of one. Using pseudo_reps does not compensate for having too few biological donors.

Common mistakes and limitations

  • Treating pseudo-replicates as biological replicates: random partitions of one donor support descriptive stability checks only. Non-independent cells or partitions counted as distinct replicates inflate the degrees of freedom and the false-positive rate of downstream models.

  • Feeding normalized values into count-based models: the log2 CPM view on this page is for visualization only. DESeq2, edgeR, and similar frameworks fit negative binomial dispersion and compute their own library-size normalization, so they need the raw integer bulk counts.

  • Ignoring matched-pair and batch structure: the downstream model must account for the paired design and batch, and its effects must be estimable. For this cohort, an intercept plus separate categorical effects for disease, pair_index_CW, and batch cannot all be estimated separately.

References
  1. Binvignat, M., Miao, B. Y., Wibrand, C., Yang, M. M., Rychkov, D., Flynn, E., Nititham, J., Tamaki, W., Khan, U., Carvidi, A., Krueger, M., Niemi, E., Sun, Y., Fragiadakis, G. K., Sellam, J., Mariotti-Ferrandiz, E., Klatzmann, D., Gross, A. J., Ye, C. J., … Sirota, M. (2024). Single-cell RNA-Seq analysis reveals cell subsets and gene signatures associated with rheumatoid arthritis disease activity. JCI Insight, 9(16). 10.1172/jci.insight.178499