Data Matrices
DataMatrix objects — annotated matrices where rows are variables and columns are observations — are central in SingleCellProjections.jl.
Let's load a sample to see what a DataMatrix looks like:
using SingleCellProjections
import SingleCellProjections as SCP
using ReproducibleJobs
using SparseArrays
sample_path = joinpath("samples", "AML28.h5")"samples/AML28.h5"counts = SCP.load_counts(sample_path; sample_names="AML28")
c = fetch!(counts)DataMatrix (32738 variables and 6905 observations)
Block Matrix (32×7)
Variables: id, name, feature_type, genome, read, pattern, sequence
Observations: cell_id, sample_name, barcodeThe display shows the matrix size (number of variables and observations), a brief description of the matrix contents, and an overview of available variable and observation annotations.
Variables
Variables, or var for short, are typically genes, features (such as CITE-seq features), or variables after dimension reduction (e.g. "UMAP1"). The variables are stored as a DataFrame and can be accessed by:
c.var[1:6, :]| Row | id | name | feature_type | genome | read | pattern | sequence |
|---|---|---|---|---|---|---|---|
| String | String | String | String | String | String | String | |
| 1 | ENSG00000243485 | MIR1302-10 | Gene Expression | hg19 | |||
| 2 | ENSG00000237613 | FAM138A | Gene Expression | hg19 | |||
| 3 | ENSG00000186092 | OR4F5 | Gene Expression | hg19 | |||
| 4 | ENSG00000238009 | RP11-34P13.7 | Gene Expression | hg19 | |||
| 5 | ENSG00000239945 | RP11-34P13.8 | Gene Expression | hg19 | |||
| 6 | ENSG00000237683 | AL627309.1 | Gene Expression | hg19 |
Observations
Observations, or obs for short, are typically cells, but can also be pseudobulk samples, for example. The observations are stored as a DataFrame and can be accessed by:
c.obs[1:6, :]| Row | cell_id | sample_name | barcode |
|---|---|---|---|
| String | String | String | |
| 1 | AML28_AAACCCACAAATCCCA-1 | AML28 | AAACCCACAAATCCCA-1 |
| 2 | AML28_AAACCCACAAGAAACT-1 | AML28 | AAACCCACAAGAAACT-1 |
| 3 | AML28_AAACCCACAAGCGCAA-1 | AML28 | AAACCCACAAGCGCAA-1 |
| 4 | AML28_AAACCCACACCTAAAC-1 | AML28 | AAACCCACACCTAAAC-1 |
| 5 | AML28_AAACCCATCCGGGACT-1 | AML28 | AAACCCATCCGGGACT-1 |
| 6 | AML28_AAACCCATCCTCTGCA-1 | AML28 | AAACCCATCCTCTGCA-1 |
IDs
Each variable and each observation must have a unique ID. The first column of the var and obs DataFrames is always the ID column.
Most of the time, IDs are handled automatically by SingleCellProjections.jl. Sometimes, you need to make sure IDs are unique when loading or merging data matrices. In particular, when loading a DataMatrix that should be projected onto another DataMatrix, the user must ensure that variable IDs match.
Matrix
The matrix can be accessed by data.matrix. Depending on the stage of analysis, different kinds of matrices (or matrix-like objects) are used. Most of this complexity is hidden from the user, but internally SingleCellProjections.jl depends on this functionality to be fast and to reduce memory usage.
SingleCellProjections.jl will reuse matrices when possible, in order to reduce memory usage. E.g. normalize_matrix will reuse and extend the Matrix Expression of the source DataMatrix, without creating a copy of the actual data. When matrices are reused/copied is considered an implementation detail, and can change at any time. Users of SingleCellProjections.jl should thus consider the matrices to be "read-only". This should rarely present problems in practice.
Roughly, the matrix types used at different stages are:
- Counts —
SparseMatrixCSC(often in a blocked format) - Transformed and normalized data — Matrix Expressions
- PCA result —
Matrix{Float64} - ForceLayout/UMAP/t-SNE result —
Matrix{Float64}
Jobs and DataMatrices
A DataMatrix Job is internally split into three component Jobs: one for the matrix, one for var, and one for obs. You can access these with:
SCP.get_matrix(job)— the matrix componentSCP.get_var(job)— the variable annotationsSCP.get_obs(job)— the observation annotations
Operations that only affect one component leave the others unchanged. For example, SCP.logtransform only transforms the matrix — it passes var and obs through without modification. This means the var and obs Jobs are literally the same object (identical by ===) before and after the transformation:
transformed = SCP.logtransform(counts)
# var is unchanged by logtransform — same Job object
forward!(SCP.get_var(transformed)) === forward!(SCP.get_var(counts))true# matrix is different — logtransform created a new matrix Job
forward!(SCP.get_matrix(transformed)) === forward!(SCP.get_matrix(counts))falseThis splitting is what makes the caching and projection system efficient — unchanged components are never recomputed or stored redundantly.