perturbMatch reference signatures via ExperimentHub

Introduction

perturbMatchData is an ExperimentHub package that provides a collection of human reference perturbation expression signatures as HDF5-backed SummarizedExperiment objects. Each signature summarises the differential expression observed after a single-gene perturbation (knockdown, knockout or overexpression) in an RNA-seq experiment.

The signatures were assembled with an automated Nextflow pipeline and summarised in two ways: using the characteristic direction method and using limma (log2 fold-change together with a pi score, log2FC * -log10(padj)). Both are provided with and without SVA batch correction, giving four datasets in total.

These reference databases are used by the perturbMatch package to identify the upstream regulators most likely to drive a query gene expression signature, and can also be used directly to ask what happens to gene expression when a given gene is perturbed.

Installation

if (!requireNamespace("BiocManager", quietly = TRUE)) {
    install.packages("BiocManager")
}
BiocManager::install("perturbMatchData")
library(perturbMatchData)
library(SummarizedExperiment)
#> Loading required package: MatrixGenerics
#> Loading required package: matrixStats
#> 
#> Attaching package: 'MatrixGenerics'
#> The following objects are masked from 'package:matrixStats':
#> 
#>     colAlls, colAnyNAs, colAnys, colAvgsPerRowSet, colCollapse,
#>     colCounts, colCummaxs, colCummins, colCumprods, colCumsums,
#>     colDiffs, colIQRDiffs, colIQRs, colLogSumExps, colMadDiffs,
#>     colMads, colMaxs, colMeans2, colMedians, colMins, colOrderStats,
#>     colProds, colQuantiles, colRanges, colRanks, colSdDiffs, colSds,
#>     colSums2, colTabulates, colVarDiffs, colVars, colWeightedMads,
#>     colWeightedMeans, colWeightedMedians, colWeightedSds,
#>     colWeightedVars, rowAlls, rowAnyNAs, rowAnys, rowAvgsPerColSet,
#>     rowCollapse, rowCounts, rowCummaxs, rowCummins, rowCumprods,
#>     rowCumsums, rowDiffs, rowIQRDiffs, rowIQRs, rowLogSumExps,
#>     rowMadDiffs, rowMads, rowMaxs, rowMeans2, rowMedians, rowMins,
#>     rowOrderStats, rowProds, rowQuantiles, rowRanges, rowRanks,
#>     rowSdDiffs, rowSds, rowSums2, rowTabulates, rowVarDiffs, rowVars,
#>     rowWeightedMads, rowWeightedMeans, rowWeightedMedians,
#>     rowWeightedSds, rowWeightedVars
#> Loading required package: GenomicRanges
#> Loading required package: stats4
#> Loading required package: BiocGenerics
#> Loading required package: generics
#> 
#> Attaching package: 'generics'
#> The following objects are masked from 'package:base':
#> 
#>     as.difftime, as.factor, as.ordered, intersect, is.element, setdiff,
#>     setequal, union
#> 
#> Attaching package: 'BiocGenerics'
#> The following objects are masked from 'package:stats':
#> 
#>     IQR, mad, sd, var, xtabs
#> The following object is masked from 'package:utils':
#> 
#>     data
#> The following objects are masked from 'package:base':
#> 
#>     anyDuplicated, aperm, append, as.data.frame, basename, cbind,
#>     colnames, dirname, do.call, duplicated, eval, evalq, Filter, Find,
#>     get, grep, grepl, is.unsorted, lapply, Map, mapply, match, mget,
#>     order, paste, pmax, pmax.int, pmin, pmin.int, Position, rank,
#>     rbind, Reduce, rownames, sapply, saveRDS, scale, sequence, table,
#>     tapply, transform, unique, unsplit, which.max, which.min
#> Loading required package: S4Vectors
#> 
#> Attaching package: 'S4Vectors'
#> The following object is masked from 'package:utils':
#> 
#>     findMatches
#> The following objects are masked from 'package:base':
#> 
#>     expand.grid, I, unname
#> Loading required package: IRanges
#> Loading required package: Seqinfo
#> Loading required package: Biobase
#> Welcome to Bioconductor
#> 
#>     Vignettes contain introductory material; view with
#>     'browseVignettes()'. To cite Bioconductor, see
#>     'citation("Biobase")', and for packages 'citation("pkgname")'.
#> 
#> Attaching package: 'Biobase'
#> The following object is masked from 'package:MatrixGenerics':
#> 
#>     rowMedians
#> The following objects are masked from 'package:matrixStats':
#> 
#>     anyMissing, rowMedians

Available datasets

The four datasets are:

Accessor Method Batch correction
perturbMatch_chrdir() Characteristic direction No
perturbMatch_chrdir_sva() Characteristic direction SVA
perturbMatch_limma() limma (log2FC / pi score) No
perturbMatch_limma_sva() limma (log2FC / pi score) SVA

Discovering the data on ExperimentHub

Most work starts from the accessors in the previous section, which handle retrieval for you. The underlying records can also be browsed like any other ExperimentHub resource, which is useful for checking what is hosted and what is already in the local cache. perturbMatch_reference() returns the records prepared by this package:

library(ExperimentHub)
#> Loading required package: AnnotationHub
#> Loading required package: BiocFileCache
#> Loading required package: dbplyr
#> 
#> Attaching package: 'AnnotationHub'
#> The following object is masked from 'package:Biobase':
#> 
#>     cache

perturbMatch_reference()
#> ExperimentHub with 9 records
#> # snapshotDate(): 2026-09-04
#> # $dataprovider: NCBI GEO
#> # $species: Homo sapiens
#> # $rdataclass: DFrame, character
#> # additional mcols(): taxonomyid, genome, description,
#> #   coordinate_1_based, maintainer, rdatadateadded, preparerclass, tags,
#> #   rdatapath, sourceurl, sourcetype 
#> # retrieve records with, e.g., 'object[["EH10451"]]' 
#> 
#>             title                          
#>   EH10451 | perturbMatch_rowData           
#>   EH10452 | perturbMatch_chrdir_colData    
#>   EH10453 | perturbMatch_chrdir_sva_colData
#>   EH10454 | perturbMatch_limma_colData     
#>   EH10455 | perturbMatch_limma_sva_colData 
#>   EH10456 | perturbMatch_chrdir_assays     
#>   EH10457 | perturbMatch_chrdir_sva_assays 
#>   EH10458 | perturbMatch_limma_assays      
#>   EH10459 | perturbMatch_limma_sva_assays

## querying the hub directly also matches on the other metadata fields
eh <- ExperimentHub()
query(eh, "perturbMatchData")
#> ExperimentHub with 9 records
#> # snapshotDate(): 2026-09-04
#> # $dataprovider: NCBI GEO
#> # $species: Homo sapiens
#> # $rdataclass: DFrame, character
#> # additional mcols(): taxonomyid, genome, description,
#> #   coordinate_1_based, maintainer, rdatadateadded, preparerclass, tags,
#> #   rdatapath, sourceurl, sourcetype 
#> # retrieve records with, e.g., 'object[["EH10451"]]' 
#> 
#>             title                          
#>   EH10451 | perturbMatch_rowData           
#>   EH10452 | perturbMatch_chrdir_colData    
#>   EH10453 | perturbMatch_chrdir_sva_colData
#>   EH10454 | perturbMatch_limma_colData     
#>   EH10455 | perturbMatch_limma_sva_colData 
#>   EH10456 | perturbMatch_chrdir_assays     
#>   EH10457 | perturbMatch_chrdir_sva_assays 
#>   EH10458 | perturbMatch_limma_assays      
#>   EH10459 | perturbMatch_limma_sva_assays

Each dataset is stored as a record of signature matrices and a record of signature annotation, alongside one gene annotation record shared by all four. Every record carries a stable ExperimentHub accession, so a dataset can also be composed by hand from those accessions.

Loading a dataset

Use the named accessors to assemble a dataset as a SummarizedExperiment. Each resource is downloaded once and subsequent calls read from the local ExperimentHub cache. The signature matrices are large, 790 MB for chrdir and 673 MB for chrdir_sva, and 3.1 GB and 2.6 GB for limma and limma_sva, which carry three assays each. The following chunk is therefore shown rather than run when this vignette is built:

se <- perturbMatch_chrdir()
se

To see which perturbations are available without retrieving those matrices, ask for the annotation alone. This is what the rest of this vignette uses:

se <- perturbMatch_chrdir(withAssays = FALSE)
#> see ?perturbMatchData and browseVignettes('perturbMatchData') for documentation
#> downloading 1 resources
#> retrieving 1 resource
#> loading from cache
#> see ?perturbMatchData and browseVignettes('perturbMatchData') for documentation
#> downloading 1 resources
#> retrieving 1 resource
#> loading from cache
se
#> class: SummarizedExperiment 
#> dim: 39376 6802 
#> metadata(9): dataset method ... assay_names assay_bytes
#> assays(0):
#> rownames(39376): 100287102 653635 ... 4576 4571
#> rowData names(18): GeneID Symbol ... GOProcess GOComponent
#> colnames(6802): GSE100009_ZZZ3_KD_1 GSE100092_HECTD1_KD_1 ...
#>   GSE99687_SMAD5_KO_1 GSE99825_CTCFL_KD_1
#> colData names(14): sig_id pert_symbol ... cell_line_efo_label
#>   target_gene_percentile

The accessors wrap the standard ExperimentHub interface, so the resources can also be retrieved by accession and composed by hand. Look the accessions up by title rather than writing them out, so that they follow the deposit:

recs <- perturbMatch_reference()
ids <- setNames(names(recs), recs$title)
ids
#>            perturbMatch_rowData     perturbMatch_chrdir_colData 
#>                       "EH10451"                       "EH10452" 
#> perturbMatch_chrdir_sva_colData      perturbMatch_limma_colData 
#>                       "EH10453"                       "EH10454" 
#>  perturbMatch_limma_sva_colData      perturbMatch_chrdir_assays 
#>                       "EH10455"                       "EH10456" 
#>  perturbMatch_chrdir_sva_assays       perturbMatch_limma_assays 
#>                       "EH10457"                       "EH10458" 
#>   perturbMatch_limma_sva_assays 
#>                       "EH10459"

What distinguishes the four datasets, including which HDF5 datasets the assay resource holds and the scoring metric, is attached to the colData resource, so composing an object by hand means carrying it across:

eh <- ExperimentHub()
h5 <- eh[[ids[["perturbMatch_chrdir_assays"]]]]
colData <- eh[[ids[["perturbMatch_chrdir_colData"]]]]

se <- SummarizedExperiment(
    assays = list(chrdir = HDF5Array::HDF5Array(h5, "chrdir")),
    rowData = eh[[ids[["perturbMatch_rowData"]]]],
    colData = colData
)
metadata(se) <- metadata(colData)

Exploring the data

Each object is a SummarizedExperiment with genes in the rows and perturbation signatures in the columns. The assays are HDF5-backed delayed arrays, so the data is read from disk on demand rather than held in memory.

## Signature-level metadata (one row per perturbation)
head(colData(se))
#> DataFrame with 6 rows and 14 columns
#>                                      sig_id pert_symbol     pert_id   pert_type
#>                                 <character> <character> <character> <character>
#> GSE100009_ZZZ3_KD_1     GSE100009_ZZZ3_KD_1        ZZZ3     ZZZ3_KD          KD
#> GSE100092_HECTD1_KD_1 GSE100092_HECTD1_KD_1      HECTD1   HECTD1_KD          KD
#> GSE100183_EP400_KD_1   GSE100183_EP400_KD_1       EP400    EP400_KD          KD
#> GSE100183_MYCL_KD_1     GSE100183_MYCL_KD_1        MYCL     MYCL_KD          KD
#> GSE100260_LIPA_KO_1     GSE100260_LIPA_KO_1        LIPA     LIPA_KO          KO
#> GSE100261_MTHFD2_KD_1 GSE100261_MTHFD2_KD_1      MTHFD2   MTHFD2_KD          KD
#>                         accession        control_samples           case_samples
#>                       <character>            <character>            <character>
#> GSE100009_ZZZ3_KD_1     GSE100009 GSM3100231,GSM310023.. GSM3100233,GSM310023..
#> GSE100092_HECTD1_KD_1   GSE100092  GSM2671101,GSM2671102 GSM2671097,GSM267109..
#> GSE100183_EP400_KD_1    GSE100183 GSM2674232,GSM267423.. GSM2674235,GSM267423..
#> GSE100183_MYCL_KD_1     GSE100183 GSM2674232,GSM267423.. GSM2674244,GSM267424..
#> GSE100260_LIPA_KO_1     GSE100260 GSM2676347,GSM267634.. GSM2676345,GSM267634..
#> GSE100261_MTHFD2_KD_1   GSE100261 GSM2676365,GSM267636.. GSM2676368,GSM267636..
#>                        technology              cell_line                  model
#>                       <character>            <character>            <character>
#> GSE100009_ZZZ3_KD_1       RNA-seq                  H1299 Qwen3-finetuned, Mag..
#> GSE100092_HECTD1_KD_1     RNA-seq                   T47D                curated
#> GSE100183_EP400_KD_1      RNA-seq                  MKL-1                curated
#> GSE100183_MYCL_KD_1       RNA-seq                  MKL-1                curated
#> GSE100260_LIPA_KO_1       RNA-seq iPSC-derived Macroph..                curated
#> GSE100261_MTHFD2_KD_1     RNA-seq Aortic Endothelial C.. Qwen3-finetuned, Mag..
#>                       entrez_gene_id cell_line_efo_id    cell_line_efo_label
#>                            <numeric>      <character>            <character>
#> GSE100009_ZZZ3_KD_1            26009      EFO:0003043              NCI-H1299
#> GSE100092_HECTD1_KD_1          25831               NA                     NA
#> GSE100183_EP400_KD_1           57634               NA                     NA
#> GSE100183_MYCL_KD_1             4610               NA                     NA
#> GSE100260_LIPA_KO_1             3988               NA                     NA
#> GSE100261_MTHFD2_KD_1          10797      EFO:0022506 immortalized human a..
#>                       target_gene_percentile
#>                                    <numeric>
#> GSE100009_ZZZ3_KD_1                 0.977568
#> GSE100092_HECTD1_KD_1              36.482872
#> GSE100183_EP400_KD_1                1.205720
#> GSE100183_MYCL_KD_1                 3.056862
#> GSE100260_LIPA_KO_1                 0.788644
#> GSE100261_MTHFD2_KD_1               1.117109

## Gene annotation data from NCBI
head(rowData(se))
#> DataFrame with 6 rows and 18 columns
#>              GeneID      Symbol            Description               Synonyms
#>           <numeric> <character>            <character>            <character>
#> 100287102 100287102     DDX11L1 DEAD/H-box helicase ..                     NA
#> 653635       653635      WASH7P WASP family homolog ..          FAM39F|WASH5P
#> 102466751 102466751   MIR6859-1        microRNA 6859-1         hsa-mir-6859-1
#> 107985730 107985730 MIR1302-2HG    MIR1302-2 host gene                     NA
#> 100302278 100302278   MIR1302-2        microRNA 1302-2 MIRN1302-2|hsa-mir-1..
#> 645520       645520     FAM138A family with sequence..           F379|FAM138F
#>              GeneType   EnsemblGeneID      Status       ChrAcc    ChrStart
#>           <character>     <character> <character>  <character> <character>
#> 100287102      pseudo ENSG00000290825      active NC_000001.11       11874
#> 653635         pseudo              NA      active NC_000001.11       14362
#> 102466751       ncRNA ENSG00000278267      active NC_000001.11       17369
#> 107985730       ncRNA              NA      active NC_000001.11       29926
#> 100302278       ncRNA ENSG00000284332      active NC_000001.11       30366
#> 645520          ncRNA ENSG00000237613      active NC_000001.11       34611
#>               ChrStop Orientation    Length GOFunctionID GOProcessID
#>           <character> <character> <numeric>  <character> <character>
#> 100287102       14409    positive      1652           NA          NA
#> 653635          29370    negative      1769           NA          NA
#> 102466751       17436    negative        68           NA          NA
#> 107985730       31295    positive       538           NA          NA
#> 100302278       30503    positive       138           NA  GO:0035195
#> 645520          36081    negative      1130           NA          NA
#>           GOComponentID  GOFunction              GOProcess GOComponent
#>             <character> <character>            <character> <character>
#> 100287102            NA          NA                     NA          NA
#> 653635               NA          NA                     NA          NA
#> 102466751            NA          NA                     NA          NA
#> 107985730            NA          NA                     NA          NA
#> 100302278            NA          NA miRNA-mediated gene ..          NA
#> 645520               NA          NA                     NA          NA

## How the scoring matrix should be derived from the assay(s)
metadata(se)$ref_metric
#> [1] "chrdir"

The signature matrix itself is only present when the object was built with withAssays = TRUE, so this chunk is again shown rather than run:

assay(se)[1:10, 1:3]

The limma datasets carry three assays (log2fc, padj and the combined pi score) and the chrdir datasets a single chrdir assay. The metadata(se)$ref_metric field records which assays downstream tools such as perturbMatch::calcEnrichment() should use.

Data provenance

The scripts used to build these objects from the signature results are included with the package.

list.files(system.file("scripts", package = "perturbMatchData"))
#> [1] "make-data.R"     "make-metadata.R" "run_nextflow.sh" "utils.R"

run_nextflow.sh records the LLM-GeoSig run that annotated the GEO series and computed the signatures from the GEO RNA-seq counts matrices, make-data.R assembles each SummarizedExperiment and make-metadata.R writes the ExperimentHub metadata.

The case and control groupings, the perturbed gene and the perturbation type were assigned by large language models rather than curated by hand, using the agreement of two models and a curated subset for calibration. Treat the colData annotation as automated annotation: it is accurate enough to rank regulators across thousands of signatures, but any individual signature should be checked against its GEO series before it is relied on.

The assembled resources are deposited on Zenodo under the concept DOI 10.5281/zenodo.21792179, which resolves to the current version of the deposit. The ExperimentHub records point at a fixed versioned record, so the files retrieved by a given release of this package do not change.

Citing the data

Please cite the Zenodo deposit above alongside the methods the signatures were computed with.

The source counts are the NCBI-generated RNA-seq counts matrices for each GEO series, described at https://www.ncbi.nlm.nih.gov/geo/info/rnaseqcounts.html.

Clark NR, Hu KS, Feldmann AS, Kou Y, Chen EY, Duan Q, Ma’ayan A. The characteristic direction: a geometrical approach to identify differentially expressed genes. BMC Bioinformatics. 2014;15:79. doi:[10.1186/1471-2105-15-79](https://doi.org/10.1186/1471-2105-15-79)

Ritchie ME, Phipson B, Wu D, Hu Y, Law CW, Shi W, Smyth GK. limma powers differential expression analyses for RNA-sequencing and microarray studies. Nucleic Acids Research. 2015;43(7):e47. doi:[10.1093/nar/gkv007](https://doi.org/10.1093/nar/gkv007)

Xiao Y, Hsiao TH, Suresh U, Chen HIH, Wu X, Wolf SE, Chen Y. A novel significance score for gene selection and ranking. Bioinformatics. 2014;30(6):801-807. doi: 10.1093/bioinformatics/btr671

Leek JT, Storey JD. Capturing heterogeneity in gene expression studies by surrogate variable analysis. PLoS Genetics. 2007;3(9):e161. doi:[10.1371/journal.pgen.0030161](https://doi.org/10.1371/journal.pgen.0030161)

Session info

sessionInfo()
#> R version 4.6.1 (2026-06-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 26.04.1 LTS
#> 
#> Matrix products: default
#> BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3 
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.32.so;  LAPACK version 3.12.0
#> 
#> locale:
#>  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
#>  [3] LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8    
#>  [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8   
#>  [7] LC_PAPER=en_US.UTF-8       LC_NAME=C                 
#>  [9] LC_ADDRESS=C               LC_TELEPHONE=C            
#> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       
#> 
#> time zone: Etc/UTC
#> tzcode source: system (glibc)
#> 
#> attached base packages:
#> [1] stats4    stats     graphics  grDevices utils     datasets  methods  
#> [8] base     
#> 
#> other attached packages:
#>  [1] ExperimentHub_3.3.2         AnnotationHub_4.3.2        
#>  [3] BiocFileCache_3.3.0         dbplyr_2.6.0               
#>  [5] SummarizedExperiment_1.43.0 Biobase_2.73.2             
#>  [7] GenomicRanges_1.65.4        Seqinfo_1.3.2              
#>  [9] IRanges_2.47.5              S4Vectors_0.51.10          
#> [11] BiocGenerics_0.59.12        generics_0.1.4             
#> [13] MatrixGenerics_1.25.0       matrixStats_1.5.0          
#> [15] perturbMatchData_0.99.0     BiocStyle_2.41.0           
#> 
#> loaded via a namespace (and not attached):
#>  [1] KEGGREST_1.53.6      xfun_0.61            bslib_0.12.0        
#>  [4] httr2_1.3.0          lattice_0.23-1       vctrs_0.7.3         
#>  [7] tools_4.6.1          curl_8.0.0           tibble_3.3.1        
#> [10] AnnotationDbi_1.75.2 RSQLite_3.53.3       blob_1.3.0          
#> [13] BiocBaseUtils_1.15.1 pkgconfig_2.0.3      Matrix_1.7-6        
#> [16] lifecycle_1.0.5      compiler_4.6.1       Biostrings_2.81.9   
#> [19] htmltools_0.5.9      sys_3.4.3            buildtools_1.0.0    
#> [22] sass_0.4.10          yaml_2.3.12          crayon_1.5.3        
#> [25] pillar_1.11.1        jquerylib_0.1.4      DelayedArray_0.39.6 
#> [28] cachem_1.1.0         abind_1.4-8          tidyselect_1.2.1    
#> [31] digest_0.6.39        purrr_1.2.2          dplyr_1.2.1         
#> [34] BiocVersion_3.24.0   maketools_1.3.2      fastmap_1.2.0       
#> [37] grid_4.6.1           cli_3.6.6            SparseArray_1.13.2  
#> [40] magrittr_2.0.5       S4Arrays_1.13.0      withr_3.0.3         
#> [43] filelock_1.0.3       rappdirs_0.3.4       bit64_4.8.6         
#> [46] rmarkdown_2.32       XVector_0.53.0       httr_1.4.9          
#> [49] bit_4.6.0            otel_0.2.0           png_0.1-9           
#> [52] memoise_2.0.1        evaluate_1.0.5       knitr_1.52          
#> [55] rlang_1.3.0          glue_1.8.1           DBI_1.3.0           
#> [58] BiocManager_1.30.27  jsonlite_2.0.0       R6_2.6.1