anndata.claude is an R port of Python anndata. Its first priority is
that .h5ad files round trip between R and Python.
Create an AnnData
An AnnData holds a matrix X with
observations (obs) as rows and variables (var)
as columns.
X <- matrix(as.numeric(1:12), nrow = 3)
obs <- data.frame(
cell_type = c("T", "B", "T"),
row.names = c("cell1", "cell2", "cell3")
)
var <- data.frame(
highly_variable = c(TRUE, FALSE, TRUE, FALSE),
row.names = c("gene1", "gene2", "gene3", "gene4")
)
adata <- AnnData(X, obs = obs, var = var)
adata
#> AnnData object with n_obs × n_vars = 3 × 4
#> obs: 'cell_type'
#> var: 'highly_variable'
#> layers: None (.X)Assign Slots
Annotations are aligned to obs or var and
are kept in obsm, varm, obsp,
varp and layers. Unstructured data goes in
uns.
adata$obsm[["pca"]] <- matrix(c(0.1, 0.2, 0.3, 1, 2, 3), nrow = 3)
adata$layers[["counts"]] <- X * 2
adata$uns[["note"]] <- "example"
adata$obs[["batch"]] <- c("a", "a", "b")
adata
#> AnnData object with n_obs × n_vars = 3 × 4
#> obs: 'cell_type', 'batch'
#> var: 'highly_variable'
#> uns: 'note'
#> obsm: 'pca'
#> layers: 'counts', None (.X)Subset
Indexing is 1-based, and negative indices exclude.
adata[1:2, ]
#> View of AnnData object with n_obs × n_vars = 2 × 4
#> obs: 'cell_type', 'batch'
#> var: 'highly_variable'
#> uns: 'note'
#> obsm: 'pca'
#> layers: 'counts', None (.X)
adata[-1, c("gene1", "gene3")]$shape
#> [1] 2 2Subsetting returns a View, which shares data with the original
AnnData and prints with a View of prefix.
Modifying a View makes it an independent object and leaves the original
unchanged, with a warning.
sub <- adata[1:2, ]
sub$is_view
#> [1] TRUE
sub$obs[["batch"]] <- c("x", "y")
#> Warning: Trying to modify attribute `.obs` of view, initializing view as
#> actual.
sub$is_view
#> [1] FALSE
adata$obs[["batch"]]
#> [1] "a" "a" "b"Values extracted from a View are plain R objects, so unlike Python
x <- sub$X; x[1, 1] <- 0 does not change the View.
Assign through the View (sub$X[1, 1] <- 0) instead. This
and the other differences from Python are listed in
vignette("deviations").
Write and read h5ad
path <- tempfile(fileext = ".h5ad")
write_h5ad(adata, path)
roundtrip <- read_h5ad(path)
roundtrip
#> AnnData object with n_obs × n_vars = 3 × 4
#> obs: 'cell_type', 'batch'
#> var: 'highly_variable'
#> uns: 'note'
#> obsm: 'pca'
#> layers: 'counts', None (.X)
identical(roundtrip$obs_names, adata$obs_names)
#> [1] TRUEUse compression = "gzip" to compress datasets, and
as_sparse in read_h5ad() to read a dense
X as a sparse matrix.
Backed mode
read_h5ad(backed = "r") keeps X (and
raw$X) on disk and reads only the parts that are indexed.
Everything else is read into memory. Use backed = "r+" to
write changes to X back to the file.
backed <- read_h5ad(path, backed = "r")
backed$isbacked
#> [1] TRUE
backed$filename
#> [1] "/tmp/Rtmpt1fbxk/file1e8dbeaee61.h5ad"
backed$X[1:2, c(1, 3)]
#> [,1] [,2]
#> [1,] 1 7
#> [2,] 2 8chunked_X() visits the rows of X in chunks
and chunk_X() returns random or chosen rows.
chunks <- backed$chunked_X(2)
length(chunks)
#> [1] 2
chunks[[2]]$start
#> [1] 3
backed$chunk_X(I(3))
#> [,1] [,2] [,3] [,4]
#> [1,] 3 6 9 12to_memory() loads the whole object and
write_h5ad() or copy(filename) write a backed
object to a new file. Assigning X ends backed mode, and the
file is left unchanged.
in_memory <- backed$to_memory()
in_memory$isbacked
#> [1] FALSE
backed$file$close()sparse_dataset() makes a handle on a sparse matrix in an
open rhdf5 group, which also has $append() to add rows or
columns on disk.
Move a file to Python and back
The file written above can be read by Python anndata directly. These chunks need Python so they are shown but not run.
import anndata as ad
adata = ad.read_h5ad("example.h5ad")
adata.obs["from_python"] = True
adata.write_h5ad("from-python.h5ad")
back <- read_h5ad("from-python.h5ad")
back$obs[["from_python"]]Write and read zarr
write_zarr() writes a zarr v3 store (a directory) and
read_zarr() reads v3 and v2 stores.
zarr_path <- tempfile(fileext = ".zarr")
write_zarr(adata, zarr_path)
zarr_roundtrip <- read_zarr(zarr_path)
zarr_roundtrip
#> AnnData object with n_obs × n_vars = 3 × 4
#> obs: 'cell_type', 'batch'
#> var: 'highly_variable'
#> uns: 'note'
#> obsm: 'pca'
#> layers: 'counts', None (.X)
identical(zarr_roundtrip$obs_names, adata$obs_names)
#> [1] TRUEUse the compressors argument to choose the compression
(zstd, gzip, blosc or none) and shards to set the shard
shape. Writing zarr v2 is not yet supported.
Lazy reading
read_lazy() reads an h5ad file or zarr store into an
AnnData whose X, layers, mappings,
obs and var are lazy. Arrays are
DelayedArrays (sparse ones stay sparse) and data frame
columns are read when they are used. uns is read into
memory.
lazy <- read_lazy(zarr_path)
class(lazy$X)
#> [1] "DelayedMatrix"
#> attr(,"package")
#> [1] "DelayedArray"
lazy$X[1:2, ]
#> <2 x 4> DelayedMatrix object of type "double":
#> [,1] [,2] [,3] [,4]
#> [1,] 1 4 7 10
#> [2,] 2 5 8 11Subsetting, Views, copy() and t() stay
lazy, and to_memory() reads everything. Writing a lazy
object is not supported yet, so call to_memory() first.
lazy_memory <- lazy$to_memory()
class(lazy_memory$X)
#> [1] "matrix" "array"read_elem_lazy() does the same for a single Element, and
chunks sets the chunk shape of the
DelayedArray grid.
read_elem_lazy(zarr_path, "X", chunks = c(2, NA))
#> <3 x 4> DelayedMatrix object of type "double":
#> [,1] [,2] [,3] [,4]
#> [1,] 1 4 7 10
#> [2,] 2 5 8 11
#> [3,] 3 6 9 12Concatenate
concat() combines AnnData objects along
axis ("obs" by default) and follows Python’s
concat(). An inner join keeps the variables every object
has and an outer join keeps all of them, filling new entries with
fill_value.
a <- AnnData(
matrix(1, nrow = 2, ncol = 2),
obs = data.frame(row.names = c("a1", "a2")),
var = data.frame(row.names = c("g1", "g2"))
)
b <- AnnData(
matrix(2, nrow = 2, ncol = 2),
obs = data.frame(row.names = c("b1", "b2")),
var = data.frame(row.names = c("g2", "g3"))
)
concat(list(a, b))$var_names
#> [1] "g2"
concat(list(a, b), join = "outer")$X
#> [,1] [,2] [,3]
#> [1,] 1 1 NA
#> [2,] 1 1 NA
#> [3,] NA 2 2
#> [4,] NA 2 2A named list gives the keys, which label
stores in obs and index_unique adds to the
names.
combined <- concat(
list(first = a, second = b),
label = "batch",
index_unique = "-"
)
combined$obs
#> DataFrame with 4 rows and 1 column
#> batch
#> <factor>
#> a1-first first
#> a2-first first
#> b1-second second
#> b2-second secondmerge and uns_merge choose which
annotations of the other axis and which uns entries are
kept ("same", "unique", "first"
or "only", or a function). Views and backed objects can be
concatenated, and objects from read_lazy() give a lazy
result.
Other formats
read_text() and read_csv() read delimited
text, from a path or a character vector of lines. The first column holds
the obs names when it is not numeric and dtype
(default "float32") sets the type of X.
read_mtx() (Matrix Market),
read_umi_tools(), read_hdf() and
read_excel() (needs readxl) read other sources, and all
return an AnnData. read_umi_tools() gives a
sparse X with sorted names.
path <- tempfile(fileext = ".tsv")
writeLines(
c("gene\tcell\tcount", "g2\tc2\t3", "g1\tc1\t1", "g3\tc1\t2"),
path
)
read_umi_tools(path)
#> AnnData object with n_obs × n_vars = 2 × 3
#> layers: None (.X)write_csvs() writes obs, var,
obsm, varm and uns (and X with
skip_data = FALSE) as csv files that pandas reads the same
as Python’s output.
dir <- tempfile("csvs")
write_csvs(adata, dir)
list.files(dir, recursive = TRUE)
#> [1] "obs.csv" "obsm.csv" "uns/note.csv" "var.csv" "varm.csv"
unlink(dir, recursive = TRUE)Move a zarr store to Python and back
Python anndata reads and writes the same store. These chunks need Python so they are shown but not run.
import anndata as ad
adata = ad.read_zarr("example.zarr")
adata.obs["from_python"] = True
adata.write_zarr("from-python.zarr")
back <- read_zarr("from-python.zarr")
back$obs[["from_python"]]