| Title: | Estimate Genetic Sex from Expression Counts |
|---|---|
| Description: | Infers the genetic sex of samples, donors or cells from bulk or single-cell RNA sequencing counts. Accepts dense or sparse matrices, delimited text files, 10x Genomics outputs, and 'Seurat', 'SingleCellExperiment' and 'SummarizedExperiment' objects. |
| Authors: | Saket Choudhary [aut, cre, cph] (ORCID: <https://orcid.org/0000-0001-5202-7633>) |
| Maintainer: | Saket Choudhary <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.1.0 |
| Built: | 2026-10-21 06:19:51 UTC |
| Source: | https://github.com/saketlab/SexSeek |
Joins EstimateSex() calls to the sex recorded for each unit. Only
female/male (or F/M, any case) are scored; other labels are kept as
recorded.
CompareSex(calls, recorded)CompareSex(calls, recorded)
calls |
Output of |
recorded |
Recorded sex as a character vector named by unit, matching
|
calls with three columns added: recorded (as given),
recorded_sex ("female", "male" or NA) and agrees: TRUE when the
verdict is the recorded sex, FALSE when it is the other sex, and NA
when the call abstains (uncertain, possible_mixed, unknown) or no
usable sex is recorded.
ex <- ExampleData("mouse_bulk") calls <- EstimateSex(ex$counts, species = ex$species, group = colnames(ex$counts)) cmp <- CompareSex(calls, ex$recorded) table(recorded = cmp$recorded_sex, call = cmp$verdict)ex <- ExampleData("mouse_bulk") calls <- EstimateSex(ex$counts, species = ex$species, group = colnames(ex$counts)) cmp <- CompareSex(calls, ex$recorded) table(recorded = cmp$recorded_sex, call = cmp$verdict)
Tries stable-ID prefixes first; they are unambiguous and cost one regex per species. Falls back to scoring the input symbols against each species' panel symbols, which is weaker: panels overlap heavily between close relatives, so the returned confidence is what tells you whether to trust it.
DetectSpecies(features)DetectSpecies(features)
features |
A character vector of gene identifiers or symbols, typically
|
A list with species, confidence (high, medium, low) and
method (id_prefix, symbol, or none). species is NULL when
nothing matched.
DetectSpecies(c("ENSMUSG00000086503", "ENSMUSG00000069045")) DetectSpecies(c("ENSG00000229807.13", "ENSG00000012817"))DetectSpecies(c("ENSMUSG00000086503", "ENSMUSG00000069045")) DetectSpecies(c("ENSG00000229807.13", "ENSG00000012817"))
Scores two complementary lines of evidence, expression of sex-specific chromosome markers and expression of the X-inactivation marker, and reports the state they jointly imply. It does not force a male/female answer.
## S3 method for class 'list' EstimateSex(x, ..., group = NULL, per_cell = FALSE) EstimateSex(x, ...) ## Default S3 method: EstimateSex( x, species = NULL, group = NULL, per_cell = FALSE, annotation = NULL, input_scale = c("auto", "counts", "normalised"), ... ) ## S3 method for class 'character' EstimateSex(x, ...) ## S3 method for class 'Seurat' EstimateSex( x, species = NULL, group = NULL, per_cell = FALSE, assay = NULL, annotation = NULL, input_scale = c("auto", "counts", "normalised"), ... ) ## S3 method for class 'SingleCellExperiment' EstimateSex( x, species = NULL, group = NULL, per_cell = FALSE, assay = "counts", annotation = NULL, input_scale = c("auto", "counts", "normalised"), ... ) ## S3 method for class 'SummarizedExperiment' EstimateSex( x, species = NULL, group = NULL, per_cell = FALSE, assay = "counts", annotation = NULL, input_scale = c("auto", "counts", "normalised"), ... )## S3 method for class 'list' EstimateSex(x, ..., group = NULL, per_cell = FALSE) EstimateSex(x, ...) ## Default S3 method: EstimateSex( x, species = NULL, group = NULL, per_cell = FALSE, annotation = NULL, input_scale = c("auto", "counts", "normalised"), ... ) ## S3 method for class 'character' EstimateSex(x, ...) ## S3 method for class 'Seurat' EstimateSex( x, species = NULL, group = NULL, per_cell = FALSE, assay = NULL, annotation = NULL, input_scale = c("auto", "counts", "normalised"), ... ) ## S3 method for class 'SingleCellExperiment' EstimateSex( x, species = NULL, group = NULL, per_cell = FALSE, assay = "counts", annotation = NULL, input_scale = c("auto", "counts", "normalised"), ... ) ## S3 method for class 'SummarizedExperiment' EstimateSex( x, species = NULL, group = NULL, per_cell = FALSE, assay = "counts", annotation = NULL, input_scale = c("auto", "counts", "normalised"), ... )
x |
A counts matrix, sparse matrix, data frame, file path, or a supported container (Seurat, SingleCellExperiment). |
... |
Passed to methods. |
group |
Optional grouping of columns into units, as a vector the length
of |
per_cell |
Report one row per column instead of aggregating. |
species |
Species name, scientific or common. Detected from the gene
identifiers when |
annotation |
Optional sex-chromosome table from |
input_scale |
|
assay |
Assay to read counts from. Defaults to the object's active or first assay. |
A named list of matrices is scored one element per unit; elements
may have different gene lists and are never zero-padded. group is not
accepted for a list. With per_cell = TRUE each unit is
<element>:<column>.
Absence of Y signal is not evidence of female: loss of Y in aged male
blood, shallow sequencing and incomplete annotation all look identical to a
female sample. Most species in the shipped panel have no annotated
X-inactivation marker at all, so for those a female verdict is never
available and the honest answer is uncertain.
For single-cell input the default is to aggregate, and the biological unit is
the donor, not the library. Pass group for anything that may hold more
than one donor. A male cell can carry zero Y UMIs through dropout alone, and
ambient RNA in a mixed-sex run carries Y transcripts into female droplets,
which is a direct false-male mechanism. per_cell = TRUE is therefore
unvalidated: it has not been tested against cell-level donor labels or
doublet calls, and its output should be read as evidence, not as a call.
A data frame, one row per unit, with verdict, heuristic confidence,
marker CPM, gametolog and inactivation fractions, depth and diagnostic flags.
Confidence categories are not calibrated probabilities. inact_measured
is FALSE when the species panel has no inactivation marker or the input
lacks its row; inact_score is then 0 without being measured. per_cell = TRUE
adds per_cell_unvalidated and caps confidence at medium.
path <- system.file("extdata", "example_counts.tsv.gz", package = "SexSeek") # A file path works directly; species is detected from the gene IDs. # Without `group` the whole matrix is one unit: two donors mixed. EstimateSex(path)[, c("unit", "verdict", "flags")] # One unit per sample. counts <- as.matrix(read.delim(path, row.names = 1)) res <- EstimateSex(counts, group = colnames(counts)) res[, c("unit", "verdict", "confidence", "gametolog_frac", "inact_frac")]path <- system.file("extdata", "example_counts.tsv.gz", package = "SexSeek") # A file path works directly; species is detected from the gene IDs. # Without `group` the whole matrix is one unit: two donors mixed. EstimateSex(path)[, c("unit", "verdict", "flags")] # One unit per sample. counts <- as.matrix(read.delim(path, row.names = 1)) res <- EstimateSex(counts, group = colnames(counts)) res[, c("unit", "verdict", "confidence", "gametolog_frac", "inact_frac")]
Excerpts of public GEO datasets whose sample records state each donor's
sex. Each keeps the rows EstimateSex() scores for the species plus an
OTHER row with the rest of each library, so calls match the full matrix.
Excerpts are too small for species detection; pass species.
ExampleData(dataset = NULL)ExampleData(dataset = NULL)
dataset |
A name from |
With dataset = NULL, a data frame of the available datasets.
Otherwise a list with dataset, study, species, assay, tissue,
counts (a matrix; a named list of sparse matrices, one per donor, for
single-cell; transcript rows for "mouse_tx"), recorded (the recorded
sex named by sample, as written in the record), samples (sample
accession, title and recorded sex) and tx2gene (the transcript-to-gene
table for "mouse_tx", otherwise NULL).
ExampleData() ex <- ExampleData("rat_bulk") dim(ex$counts) ex$recordedExampleData() ex <- ExampleData("rat_bulk") dim(ex$counts) ex$recorded
Each row pairs a Y gene with its X-linked homolog. Scoring uses complete pairs from the selected marker tier. A common library-size multiplier cancels in Y/(Y+X), but gene-specific capture, length, mapping and tissue effects remain. The resulting fraction requires empirical calibration.
GametologPairs()GametologPairs()
A data frame with one row per pair: scientific_name, the Y and
X gene IDs and symbols, and the panel tier of the Y gene.
gp <- GametologPairs() gp[gp$scientific_name == "Homo sapiens", c("y_gene_name", "x_gene_name")]gp <- GametologPairs() gp[gp$scientific_name == "Homo sapiens", c("y_gene_name", "x_gene_name")]
For each unit, the share of columns (cells) with at least one count across the species' scoring-tier sex-specific markers (Y in XY species, W in ZW species).
MarkerCellFraction(x, species, group = NULL) ## S3 method for class 'list' MarkerCellFraction(x, species, group = NULL) ## Default S3 method: MarkerCellFraction(x, species, group = NULL)MarkerCellFraction(x, species, group = NULL) ## S3 method for class 'list' MarkerCellFraction(x, species, group = NULL) ## Default S3 method: MarkerCellFraction(x, species, group = NULL)
x |
Counts with cells in columns (matrix, sparse matrix, data frame or file path), or a named list of them, one per unit. |
species |
Species name, scientific or common. |
group |
For a matrix, the unit of each column. |
A data frame with unit, n_cells and fraction.
ex <- ExampleData("mouse_sc") MarkerCellFraction(ex$counts, ex$species)ex <- ExampleData("mouse_sc") MarkerCellFraction(ex$counts, ex$species)
Defaults to the core tier, which is what scoring uses: a curated set of
uniquely mappable, somatic-tissue markers. The full inventory carries every
gene on the sex chromosomes and is far too permissive to call from:
ampliconic and testis-restricted Y families in particular fire on tissue, not
on genotype.
PanelFor(species, tier = "core")PanelFor(species, tier = "core")
species |
A scientific name, as it appears in |
tier |
Which tiers to keep. |
A data frame of panel rows.
PanelFor("Homo sapiens")[, c("gene_name", "role", "tier")] PanelFor("Gallus gallus")[, c("gene_name", "role", "tier")]PanelFor("Homo sapiens")[, c("gene_name", "role", "tier")] PanelFor("Gallus gallus")[, c("gene_name", "role", "tier")]
Bars of MarkerCellFraction() per unit. When recorded is given, each bar
is topped by the recorded sex (circle female, triangle male). The result is
a ggplot, so scales and theme can be replaced.
PlotCellFraction(x, species, recorded = NULL, group = NULL, main = NULL)PlotCellFraction(x, species, recorded = NULL, group = NULL, main = NULL)
x |
Counts with cells in columns (matrix, sparse matrix, data frame or file path), or a named list of them, one per unit. |
species |
Species name, scientific or common. |
recorded |
Optional recorded sex named by unit; see |
group |
For a matrix, the unit of each column. |
main |
Plot title. |
A ggplot whose data is the MarkerCellFraction() table, with
recorded_sex when recorded was given.
ex <- ExampleData("mouse_sc") PlotCellFraction(ex$counts, ex$species, ex$recorded, main = ex$study)ex <- ExampleData("mouse_sc") PlotCellFraction(ex$counts, ex$species, ex$recorded, main = ex$study)
One point per unit: sex-specific marker expression against inactivation marker expression, both as log10(CPM + 1). Colour is the call; the shape is the recorded sex when it is given (circle female, triangle male, cross when not recorded). When no unit has a measured inactivation marker (none in the species panel, or its row absent from the input), the plot has one axis, marker expression per unit.
PlotSexEvidence(calls, recorded = NULL, main = NULL)PlotSexEvidence(calls, recorded = NULL, main = NULL)
calls |
Output of |
recorded |
Optional recorded sex named by unit; see |
main |
Plot title. |
The result is a ggplot, so scales and theme can be replaced, e.g.
+ ggplot2::scale_shape_manual(values = c(female = 1, male = 2)).
A ggplot. Its data is calls, with the recorded columns when
recorded was given.
ex <- ExampleData("mouse_bulk") calls <- EstimateSex(ex$counts, species = ex$species, group = colnames(ex$counts)) PlotSexEvidence(calls, ex$recorded, main = ex$study)ex <- ExampleData("mouse_bulk") calls <- EstimateSex(ex$counts, species = ex$species, group = colnames(ex$counts)) PlotSexEvidence(calls, ex$recorded, main = ex$study)
Expects matrix.mtx, a barcode file and a feature file, each optionally
gzipped, as CellRanger writes them. Matrix::readMM does the parsing, so
this costs no dependency beyond what the package already needs.
Read10xDir(dir, feature_column = 1L)Read10xDir(dir, feature_column = 1L)
dir |
Directory holding the triplet. |
feature_column |
Which column of the feature file to use as rownames. CellRanger v3 writes id, symbol, type; column 1 is the Ensembl identifier, which is what species detection and panel matching want. |
A sparse matrix with gene rownames and barcode colnames.
dir <- file.path(tempdir(), "tenx") dir.create(dir, showWarnings = FALSE) m <- Matrix::sparseMatrix(i = c(1, 2), j = c(1, 2), x = c(5, 3), dims = c(2, 2)) Matrix::writeMM(m, file.path(dir, "matrix.mtx")) writeLines( c("ENSG00000229807\tXIST", "ENSG00000012817\tKDM5D"), file.path(dir, "features.tsv") ) writeLines(c("AAAC-1", "AAAG-1"), file.path(dir, "barcodes.tsv")) Read10xDir(dir) unlink(dir, recursive = TRUE)dir <- file.path(tempdir(), "tenx") dir.create(dir, showWarnings = FALSE) m <- Matrix::sparseMatrix(i = c(1, 2), j = c(1, 2), x = c(5, 3), dims = c(2, 2)) Matrix::writeMM(m, file.path(dir, "matrix.mtx")) writeLines( c("ENSG00000229807\tXIST", "ENSG00000012817\tKDM5D"), file.path(dir, "features.tsv") ) writeLines(c("AAAC-1", "AAAG-1"), file.path(dir, "barcodes.tsv")) Read10xDir(dir) unlink(dir, recursive = TRUE)
Handles both the CellRanger v2 layout (a single named group holding the
genome) and the v3 layout (a matrix group with a features subgroup).
Needs the hdf5r package, which is a suggested dependency.
Read10xH5(path, feature_column = c("id", "name"))Read10xH5(path, feature_column = c("id", "name"))
path |
Path to a |
feature_column |
For v3 files, |
A sparse matrix with gene rownames and barcode colnames.
path <- tempfile(fileext = ".h5") h5 <- hdf5r::H5File$new(path, mode = "w") g <- h5$create_group("matrix") g[["data"]] <- c(5L, 3L) g[["indices"]] <- c(0L, 1L) g[["indptr"]] <- c(0L, 1L, 2L) g[["shape"]] <- c(2L, 2L) g[["barcodes"]] <- c("AAAC-1", "AAAG-1") f <- g$create_group("features") f[["id"]] <- c("ENSG00000229807", "ENSG00000012817") f[["name"]] <- c("XIST", "KDM5D") h5$close_all() Read10xH5(path) unlink(path)path <- tempfile(fileext = ".h5") h5 <- hdf5r::H5File$new(path, mode = "w") g <- h5$create_group("matrix") g[["data"]] <- c(5L, 3L) g[["indices"]] <- c(0L, 1L) g[["indptr"]] <- c(0L, 1L, 2L) g[["shape"]] <- c(2L, 2L) g[["barcodes"]] <- c("AAAC-1", "AAAG-1") f <- g$create_group("features") f[["id"]] <- c("ENSG00000229807", "ENSG00000012817") f[["name"]] <- c("XIST", "KDM5D") h5$close_all() Read10xH5(path) unlink(path)
Accepts a scientific name, a common name, or any alias in the shipped alias table, in any case.
ResolveSpecies(name)ResolveSpecies(name)
name |
A species name. |
The scientific name, or NULL if unrecognised.
ResolveSpecies("mouse") ResolveSpecies("CHICKEN") ResolveSpecies("Danio rerio")ResolveSpecies("mouse") ResolveSpecies("CHICKEN") ResolveSpecies("Danio rerio")
Returns a chromosome inventory, not a diagnostic marker panel. Pass the
result to EstimateSex(annotation = ...) to reconcile curated markers
against this annotation. Fetch once and reuse across datasets.
SexChromosomeGenes( species, release = NULL, assembly = NULL, source = NULL, mapping = NULL )SexChromosomeGenes( species, release = NULL, assembly = NULL, source = NULL, mapping = NULL )
species |
Scientific or common species name. |
release, assembly, source
|
Annotation selection passed to
|
mapping |
Optional local annotation with |
A data frame with id, name, chr, and biotype columns.
# A local mapping avoids any download. mapping <- data.frame( id = c("ENSG00000229807", "ENSG00000012817", "ENSG00000075624"), name = c("XIST", "KDM5D", "ACTB"), chr = c("X", "Y", "7"), biotype = c("lncRNA", "protein_coding", "protein_coding") ) SexChromosomeGenes("human", mapping = mapping)# A local mapping avoids any download. mapping <- data.frame( id = c("ENSG00000229807", "ENSG00000012817", "ENSG00000075624"), name = c("XIST", "KDM5D", "ACTB"), chr = c("X", "Y", "7"), biotype = c("lncRNA", "protein_coding", "protein_coding") ) SexChromosomeGenes("human", mapping = mapping)
One row per sex-linked gene. role is one of Y, X, W, Z,
inactivation (XIST, RSX) or male_specific (roX1, roX2).
SexPanels()SexPanels()
A data frame with one row per gene and columns including
scientific_name, gene_id, gene_name, chr, role and tier.
panels <- SexPanels() table(panels$role)panels <- SexPanels() table(panels$role)
Supported species and their sex-determination systems
SexSpecies()SexSpecies()
A data frame with one row per species, including its
sex-determination system, heterogametic sex and panel status.
head(SexSpecies()[, c("scientific_name", "common_name", "system", "status")])head(SexSpecies()[, c("scientific_name", "common_name", "system", "status")])
Count agreements with recorded sex
SummariseComparison(x)SummariseComparison(x)
x |
Output of |
One row per comparison: samples, and how many calls agree,
disagree or abstain (including samples with no usable recorded sex).
ex <- ExampleData("rat_bulk") calls <- EstimateSex(ex$counts, species = ex$species, group = colnames(ex$counts)) SummariseComparison(CompareSex(calls, ex$recorded))ex <- ExampleData("rat_bulk") calls <- EstimateSex(ex$counts, species = ex$species, group = colnames(ex$counts)) SummariseComparison(CompareSex(calls, ex$recorded))