| Title: | Bioconductor DuckDB Integration and High-Level I/O |
|---|---|
| Description: | Integration package providing high-level Parquet I/O functions and optimized methods for single-cell analysis workflows using DuckDB-backed data structures. Includes readParquet and writeParquet functions for seamless serialization of SummarizedExperiment, SingleCellExperiment, MultiAssayExperiment, and MultiAssaySpatialExperiment objects, plus SQL-optimized implementations of scran and scuttle methods for variance modeling, marker detection, QC metrics, and normalization. This package brings together DuckDBDataFrame, DuckDBArray, DuckDBGRanges, and DuckDBSpatial (optional) for complete Bioconductor integration. |
| Authors: | Patrick Aboyoun [aut, cre], Genentech, Inc. [cph] |
| Maintainer: | Patrick Aboyoun <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.99.26 |
| Built: | 2026-10-15 23:17:15 UTC |
| Source: | https://github.com/bioc/BiocDuckDB |
DuckDBDataFrame methods for MultiAssaySpatialExperiment spatial generics. Lazy layers delegate to DuckDBSpatial for SQL-backed spatial operations.
spatialOverlaps() returns the pairwise spatial-overlap result between
the lazy layers, spatialMatch() the matched table, and
spatialJoin() a sparse pair table (mapping row indices between layers)
when sparse = TRUE, or the joined table otherwise. All are computed
lazily over the DuckDB-backed layers.
spatialOverlaps(x, y, coords = NULL, geom = "geometry"):Test spatial overlap between lazy spatial layers via
layerSpatialOverlaps.
spatialMatch(x, table, coords, geom = "geometry", join = NULL):Match points or geometries to a table via
layerSpatialMatch.
spatialJoin(x, y, join = st_intersects, sparse = TRUE):Spatial join between lazy layers. When sparse = TRUE, returns a
sparse pair table via st_intersects_table;
otherwise uses st_join.
spatialOverlaps,
spatialMatch, and
spatialJoin for generic
documentation.
# Spatial predicates need DuckDB's optional spatial extension. Probe for it so # the example runs only where the extension can actually load; it is skipped # cleanly when offline or behind a firewall that blocks the extension # repository, or when DuckDBSpatial is not installed. spatial_ok <- requireNamespace("DuckDBSpatial", quietly = TRUE) && tryCatch({ DBI::dbGetQuery(DuckDBDataFrame::acquireDuckDBConn(), "SELECT ST_Area(ST_GeomFromText('POLYGON((0 0,0 1,1 1,1 0,0 0))'))") TRUE }, error = function(e) FALSE) if (spatial_ok) { df <- data.frame(id = 1:3, x = 1:3, y = 1:3) path <- tempfile(fileext = ".parquet") arrow::write_parquet(df, path) ddb <- DuckDBDataFrame(path, datacols = c("x", "y"), keycol = "id") polygon <- "POLYGON((0 0, 6 0, 6 6, 0 6, 0 0))" spatialOverlaps(ddb, polygon, coords = c("x", "y")) unlink(path) }# Spatial predicates need DuckDB's optional spatial extension. Probe for it so # the example runs only where the extension can actually load; it is skipped # cleanly when offline or behind a firewall that blocks the extension # repository, or when DuckDBSpatial is not installed. spatial_ok <- requireNamespace("DuckDBSpatial", quietly = TRUE) && tryCatch({ DBI::dbGetQuery(DuckDBDataFrame::acquireDuckDBConn(), "SELECT ST_Area(ST_GeomFromText('POLYGON((0 0,0 1,1 1,1 0,0 0))'))") TRUE }, error = function(e) FALSE) if (spatial_ok) { df <- data.frame(id = 1:3, x = 1:3, y = 1:3) path <- tempfile(fileext = ".parquet") arrow::write_parquet(df, path) ddb <- DuckDBDataFrame(path, datacols = c("x", "y"), keycol = "id") polygon <- "POLYGON((0 0, 6 0, 6 6, 0 6, 0 0))" spatialOverlaps(ddb, polygon, coords = c("x", "y")) unlink(path) }
The DuckDBDualSubset class conforms to the DualSubset class from SingleCellExperiment to provide lazy node-based subsetting for DuckDBSelfHits objects.
DuckDBDualSubset is a lightweight wrapper around
DuckDBSelfHits
that provides node-based subsetting semantics required by
SingleCellExperiment's colPairs/rowPairs framework. Unlike the
DualSubset class which materializes data immediately upon subsetting,
DuckDBDualSubset delegates to the nodes slot in the wrapped
DuckDBSelfHits object, enabling lazy SQL-based edge filtering.
When a DuckDBDualSubset is subset via x[i], it delegates to
extractNODES(x@hits, i), which updates the nodes slot in the
DuckDBSelfHits object. Edge filtering is deferred until materialization
via SQL WHERE clauses (only edges where BOTH from and to are in the node subset
are retained).
The DuckDBDualSubset() constructor returns a DuckDBDualSubset object
wrapping a DuckDBSelfHits.
length() returns the number of nodes (an integer); subsetting
([) returns a DuckDBDualSubset, and the [<- and c()
methods return the updated DuckDBDualSubset.
DuckDBDualSubset(hits):Creates a DuckDBDualSubset object wrapping a DuckDBSelfHits object.
hitsA DuckDBSelfHits object containing the edge data.
length(x):Returns the number of nodes (nnode(x@hits)).
x[i]:Performs node-based subsetting by delegating to
extractNODES(x@hits, i). This updates the nodes slot
in the wrapped DuckDBSelfHits object and applies lazy SQL filtering
to retain only edges where BOTH endpoints are in the subset.
DuckDBDualSubset objects are typically created automatically when assigning
DuckDBSelfHits objects to colPairs/rowPairs:
Patrick Aboyoun
DuckDBSelfHits for the underlying edge storage class.
extractNODES for the node-based subsetting method.
library(SingleCellExperiment) sce <- SingleCellExperiment(assays = list(counts = matrix(1:50, 10, 5))) hits <- S4Vectors::SelfHits(from = 1:3, to = 2:4, nnode = 5L) tmp <- tempfile() writeParquet(hits, tmp) ddb_hits <- DuckDBSelfHits(tmp, from = "from", to = "to", nnode = 5L) colPair(sce, "knn") <- ddb_hits ds <- colPair(sce, "knn") length(ds) ds[1:3] unlink(tmp, recursive = TRUE)library(SingleCellExperiment) sce <- SingleCellExperiment(assays = list(counts = matrix(1:50, 10, 5))) hits <- S4Vectors::SelfHits(from = 1:3, to = 2:4, nnode = 5L) tmp <- tempfile() writeParquet(hits, tmp) ddb_hits <- DuckDBSelfHits(tmp, from = "from", to = "to", nnode = 5L) colPair(sce, "knn") <- ddb_hits ds <- colPair(sce, "knn") length(ds) ds[1:3] unlink(tmp, recursive = TRUE)
DuckDBIrlbaParam is a BiocSingularParam
for runSVD that, for a DuckDBMatrix,
materializes the matrix once into an in-memory sparse matrix and drives
irlba directly on it, instead of letting irlba's Lanczos
iteration call %*% on the lazy DuckDBMatrix once per
solver step.
DuckDBIrlbaParam( memory_limit = initializeOptions("memory_limit"), deferred = FALSE, fold = Inf, extra.work = 7, ... ) ## S4 method for signature 'DuckDBIrlbaParam' runSVD( x, k, nu = k, nv = k, center = FALSE, scale = FALSE, BPPARAM = SerialParam(), ..., BSPARAM = ExactParam() )DuckDBIrlbaParam( memory_limit = initializeOptions("memory_limit"), deferred = FALSE, fold = Inf, extra.work = 7, ... ) ## S4 method for signature 'DuckDBIrlbaParam' runSVD( x, k, nu = k, nv = k, center = FALSE, scale = FALSE, BPPARAM = SerialParam(), ..., BSPARAM = ExactParam() )
memory_limit |
Numeric scalar, see the |
deferred, fold, extra.work, ...
|
Passed to
|
x |
A matrix-like object, typically a DuckDBMatrix (the
fast path only applies to one; any other input falls back to ordinary
|
k, nu, nv, center, scale, BPPARAM, BSPARAM
|
See
|
DuckDBMatrix already implements %*%/
crossprod as correct SQL-pushdown methods (see DuckDBArray), so
BiocSingular::runSVD(x, BSPARAM = BiocSingular::IrlbaParam()) already
computes a correct PCA/SVD directly on a DuckDBMatrix. But each of
those %*% calls pays real SQL query-construction cost, and
irlba's Lanczos loop calls it once per solver iteration, hundreds of
times per solve, so the ordinary IrlbaParam path is markedly slower
on a DuckDBMatrix than the same computation in memory (measured: on
a real 12,500-cell x 200-HVG benchmark, ~16s vs ~0.3s in-memory).
DuckDBIrlbaParam closes that gap for matrices that fit in memory, by
reusing the same loadIntoMemory machinery
(.duckdb_seed_to_sparse_matrix(), size-gated by
"memory_limit") to materialize the matrix exactly once, then calling
BiocSingular's own exported irlba-driving function
(runIrlbaSVD) directly on the materialized
matrix. Reusing that function, rather than reimplementing its nu/nv
trimming, edge cases, and output packaging, avoids a larger and riskier
undertaking than depending on one stable, public function.
Whenever the fast path is not applicable (x is not a
DuckDBMatrix, its seed is not zero-filled, or the estimated
materialized size is over "memory_limit"), this falls back to
ordinary IrlbaParam behavior (the lazy SQL-pushdown %*%
path), so it can never turn previously-working (if slower) code into
a failure. It can still fail the same way the ordinary path already does,
e.g. a non-zero-filled seed is not supported by %*% either, that
case falls back to, and fails identically to, the ordinary path rather than
silently producing an incorrect result.
A DuckDBIrlbaParam object, for use as the BSPARAM
argument to runSVD (and so
scran::fixedPCA(), scater::runPCA()/calculatePCA()).
Patrick Aboyoun
IrlbaParam for the ordinary, lazy-pushdown behavior
loadIntoMemory for the materialization helper this reuses
initializeOptions for the shared "memory_limit" default
df <- data.frame(row = c("r1", "r2", "r1"), col = c("c1", "c1", "c2"), value = c(1, 2, 3)) tf <- tempfile(fileext = ".parquet") on.exit(unlink(tf)) arrow::write_parquet(df, tf) pqmat <- DuckDBArray::DuckDBMatrix(tf, datacol = "value", keycols = list(row = c("r1", "r2"), col = c("c1", "c2"))) BiocSingular::runSVD(pqmat, k = 1, BSPARAM = DuckDBIrlbaParam())df <- data.frame(row = c("r1", "r2", "r1"), col = c("c1", "c1", "c2"), value = c(1, 2, 3)) tf <- tempfile(fileext = ".parquet") on.exit(unlink(tf)) arrow::write_parquet(df, tf) pqmat <- DuckDBArray::DuckDBMatrix(tf, datacol = "value", keycols = list(row = c("r1", "r2"), col = c("c1", "c2"))) BiocSingular::runSVD(pqmat, k = 1, BSPARAM = DuckDBIrlbaParam())
scran methods for DuckDBMatrix objects.
The DuckDBMatrix methods compute per-gene statistics using SQL aggregation, which is efficient for large disk-backed matrices:
For pairwiseTTests: per-group means and variances via SQL
For pairwiseBinom: per-group detection counts via SQL
The trend fitting and p-value computations are then performed in R using the standard scran functions.
For findMarkers, both test.type="t" and test.type="binom"
use optimized DuckDBMatrix implementations. The Wilcoxon test
(test.type="wilcox") falls back to the default method because it
requires pairwise cell comparisons that are not efficiently expressed in SQL.
These methods mirror their scran generics. correlatePairs()
returns a DataFrame of gene pairs with columns gene1, gene2,
rho (Pearson's r), p.value, and FDR.
modelGeneVar(), modelGeneVarByPoisson(), and
modelGeneCV2() return a DataFrame of per-gene variance
statistics, with the fitted mean-variance trend in metadata().
pairwiseTTests() and pairwiseBinom() return a list with
statistics and pairs elements; findMarkers() and
scoreMarkers() return a List of per-group
DataFrames of marker statistics; and summaryMarkerStats()
returns a DataFrame summarizing marker statistics per group.
The following pairwise correlation methods have optimized DuckDBMatrix implementations:
correlatePairs(x, subset.row = NULL, pairings = NULL,
use.names = TRUE, BPPARAM = SerialParam()):Compute pairwise Pearson correlations between genes using sparse-aware
SQL aggregation, with a standard t-test significance for each
correlation (p.value, BH-adjusted as FDR). Only
fill = 0 is supported. This is Pearson's r on x as given
(typically log-normalized values), not scran's Spearman's rho on
ranked expression – results are not expected to numerically match
scran::correlatePairs().
subset.rowrows (genes) to correlate; required for >1000 genes
pairingsoptional matrix specifying specific gene pairs
use.nameswhether to use row names in output
The following variance modelling methods have optimized DuckDBMatrix implementations:
modelGeneVar(x, block = NULL, design = NULL, subset.row = NULL,
subset.fit = NULL, ..., equiweight = TRUE, method = "fisher",
BPPARAM = SerialParam()):Model the variance of log-expression profiles for each gene, decomposing it into technical and biological components. Uses SQL aggregation for computing per-gene means and variances.
blockfactor specifying blocking levels for each cell
designdesign matrix for blocking (not supported with block)
subset.rowrows for which to model the variance
subset.fitrows to be used for trend fitting
equiweightwhether blocks should be weighted equally
methodmethod for combining p-values across blocks
modelGeneVarByPoisson(x, size.factors = NULL, block = NULL,
design = NULL, subset.row = NULL, ..., equiweight = TRUE, method = "fisher",
BPPARAM = SerialParam()):Model the variance of log-expression profiles for each gene, using a Poisson-based trend to estimate technical variance. Uses SQL aggregation for computing per-gene means and variances.
size.factorsnumeric vector of size factors for normalization
blockfactor specifying blocking levels for each cell
designdesign matrix for blocking (not supported with block)
subset.rowrows for which to model the variance
equiweightwhether blocks should be weighted equally
methodmethod for combining p-values across blocks
modelGeneCV2(x, size.factors = NULL, block = NULL,
subset.row = NULL, subset.fit = NULL, ..., equiweight = TRUE, method = "fisher",
BPPARAM = SerialParam()):Model the squared coefficient of variation (CV2) of normalized expression for each gene, fitting a trend to account for the mean-CV2 relationship. Uses SQL aggregation for computing per-gene means and variances on size-factor normalized counts.
size.factorsnumeric vector of size factors for normalization
blockfactor specifying blocking levels for each cell
subset.rowrows for which to model the CV2
subset.fitrows to be used for trend fitting
equiweightwhether blocks should be weighted equally
methodmethod for combining p-values across blocks
The following marker gene detection methods have optimized DuckDBMatrix implementations:
pairwiseTTests(x, groups, block = NULL, design = NULL,
restrict = NULL, exclude = NULL, direction = c("any", "up", "down"),
lfc = 0, std.lfc = FALSE, log.p = FALSE, gene.names = NULL,
subset.row = NULL, BPPARAM = SerialParam()):Perform pairwise Welch t-tests between groups of cells. Uses SQL aggregation for computing per-gene, per-group means and variances.
groupsvector specifying group assignment for each cell
blockfactor specifying blocking levels
restrictcharacter vector of groups to restrict comparisons to
excludecharacter vector of groups to exclude from comparisons
directiondirection of log-fold changes to consider
lfclog-fold change threshold
std.lfcwhether to standardize log-fold changes
log.pwhether to return log-transformed p-values
gene.namescharacter vector of gene names for output
pairwiseBinom(x, groups, block = NULL, restrict = NULL,
exclude = NULL, direction = c("any", "up", "down"), threshold = 1e-8,
lfc = 0, log.p = FALSE, gene.names = NULL, subset.row = NULL,
BPPARAM = SerialParam()):Perform pairwise binomial tests between groups of cells. Uses SQL aggregation for computing per-gene, per-group detection counts.
groupsvector specifying group assignment for each cell
blockfactor specifying blocking levels
restrictcharacter vector of groups to restrict comparisons to
excludecharacter vector of groups to exclude from comparisons
directiondirection of log-fold changes to consider
thresholdexpression threshold for detection
lfclog-fold change threshold for proportions
log.pwhether to return log-transformed p-values
gene.namescharacter vector of gene names for output
findMarkers(x, groups, test.type = c("t", "wilcox", "binom"), ...,
pval.type = c("any", "some", "all"), min.prop = NULL, log.p = FALSE,
full.stats = FALSE, sorted = TRUE, row.data = NULL, add.summary = FALSE,
BPPARAM = SerialParam()):Find candidate marker genes for groups of cells.
groupsvector specifying group assignment for each cell
test.typetype of pairwise test ("t" and "binom" are optimized)
pval.typehow to combine p-values across pairwise tests
sortedwhether to sort results by significance
add.summarywhether to add summary statistics
scoreMarkers(x, groups, block = NULL, pairings = NULL, lfc = 0,
row.data = NULL, full.stats = FALSE, subset.row = NULL,
BPPARAM = SerialParam(), true.auc = FALSE):Compute effect size summary statistics for potential marker genes. Uses SQL aggregation for computing per-gene, per-group means, variances, and detection proportions.
AUC computation options: By default (true.auc = FALSE), AUC is
computed using a normal approximation which is fast (~97% correlation with scran).
Set true.auc = TRUE to compute true rank-based AUC using SQL window functions
(Wilcoxon rank-sum statistic). This is slower (~15-25x) but provides exact AUC
values identical to scran (100% correlation).
groupsvector specifying group assignment for each cell
blockfactor specifying blocking levels
pairingsspecification of pairwise comparisons to compute
lfclog-fold change threshold for effect sizes
row.dataadditional row data to include in output
full.statswhether to return full pairwise statistics
true.auclogical indicating whether to compute true rank-based AUC via SQL window functions (default FALSE uses normal approximation with ~97% correlation; TRUE gives 100% exact match but is ~15-25x slower)
summaryMarkerStats(x, groups, row.data = NULL, average = "mean",
BPPARAM = SerialParam()):Compute summary statistics for marker gene selection. Uses SQL aggregation for per-group means and detection proportions.
groupsvector specifying group assignment for each cell
row.dataadditional row data to include in output
averagetype of average to compute ("mean" or "median")
The scoreMarkers method for DuckDBMatrix provides two AUC computation modes:
Normal approximation (default, true.auc = FALSE):
where is the standard normal CDF. This is fast but may be inaccurate for
sparse/bimodal data.
True rank-based AUC (true.auc = TRUE):
Computes exact AUC via the Wilcoxon rank-sum statistic using SQL window functions:
where is the sum of ranks in group 1. This is ~15-25x slower than the
normal approximation but provides exact results identical to scran (correlation = 1.0).
Benchmarks (5000 genes x 5000 cells, 12 pairwise comparisons):
Default (normal approx): 0.68s, ~97% correlation with scran
true.auc = TRUE: 17.7s, 100% exact match with scran
The true AUC is preferred for:
Genes with bimodal expression (expressed vs. not expressed)
Sparse data with many zeros
Applications where exact effect sizes are important
Alternatively, use findMarkers(x, groups, test.type = "wilcox") for
rank-based p-values.
Patrick Aboyoun
DuckDBMatrix-class for the main class
DuckDBMatrix-scuttle for the scuttle methods
correlatePairs for the scran generic
modelGeneVar for the scran generic
modelGeneVarByPoisson for the scran generic
modelGeneCV2 for the scran generic
pairwiseTTests for the scran generic
pairwiseBinom for the scran generic
findMarkers for the scran generic
scoreMarkers for the scran generic
summaryMarkerStats for the scran generic
mat <- matrix(rpois(500, 5), 25, 20) dimnames(mat) <- list(paste0("G", 1:25), paste0("C", 1:20)) names(dimnames(mat)) <- c("index1", "index2") tmp <- tempfile() writeParquet(mat, tmp) pqmat <- DuckDBMatrix(tmp, datacol = "value", keycols = lapply(dimnames(mat), function(x) setNames(seq_along(x), x))) head(scran::modelGeneVarByPoisson(pqmat)) unlink(tmp, recursive = TRUE)mat <- matrix(rpois(500, 5), 25, 20) dimnames(mat) <- list(paste0("G", 1:25), paste0("C", 1:20)) names(dimnames(mat)) <- c("index1", "index2") tmp <- tempfile() writeParquet(mat, tmp) pqmat <- DuckDBMatrix(tmp, datacol = "value", keycols = lapply(dimnames(mat), function(x) setNames(seq_along(x), x))) head(scran::modelGeneVarByPoisson(pqmat)) unlink(tmp, recursive = TRUE)
scuttle methods for DuckDBMatrix objects.
These methods mirror their scuttle generics.
perCellQCMetrics() and perFeatureQCMetrics() return a
DataFrame of QC metrics. librarySizeFactors(),
geometricSizeFactors(), and calculateAverage() return a numeric
vector. normalizeCounts(), calculateCPM(), and
calculateTPM() return a matrix-like object of transformed values, and
numDetectedAcrossFeatures() and sumCountsAcrossFeatures() a
matrix-like object of per-group summaries. summarizeAssayByGroup()
returns a
SummarizedExperiment
of per-group assay summaries.
The following QC metrics methods have optimized DuckDBMatrix implementations:
perCellQCMetrics(x, subsets = NULL, percent.top = integer(0),
threshold = 0, ..., flatten = TRUE):Compute per-cell quality control metrics for a count matrix.
subsetsnamed list of vectors specifying feature subsets (e.g., mitochondrial genes). Each vector can be character (feature names), logical, or integer (indices). Returns sum, detected, and percent for each subset.
percent.topinteger vector specifying the sizes of the top
sets of high-abundance genes (e.g., c(50, 100, 200)). For each
size, computes the percentage of total counts assigned to the most
highly expressed genes in each cell. Uses SQL window functions for
efficient computation.
thresholdnumeric scalar specifying the detection threshold
flattenlogical scalar indicating whether nested DataFrames
in the output should be flattened. If TRUE (default), subset
and percent.top statistics are returned as top-level columns
(e.g., subsets_Mito_sum, percent.top_50).
If FALSE, nested DataFrames/matrices are returned.
perFeatureQCMetrics(x, subsets = NULL, threshold = 0, ..., flatten = TRUE):Compute per-feature quality control metrics for a count matrix.
subsetsnamed list of vectors specifying cell subsets (e.g., control wells). Each vector can be character (cell names), logical, or integer (indices). Returns mean, detected, and ratio for each subset.
thresholdnumeric scalar specifying the detection threshold
flattenlogical scalar indicating whether nested DataFrames
in the output should be flattened. If TRUE (default), subset
statistics are returned as top-level columns (e.g., subsets_SetA_mean).
If FALSE, a nested subsets DataFrame is returned.
The following normalization methods have optimized DuckDBMatrix implementations:
librarySizeFactors(x):Define per-cell size factors from the library sizes.
geometricSizeFactors(x, pseudo.count = 1):Define per-cell size factors from the geometric mean of counts per cell.
pseudo.countnumeric scalar specifying the pseudo-count to add during log-transformation
normalizeCounts(x, size.factors = NULL, log = TRUE, transform = c("log", "none", "asinh"),
pseudo.count = 1, center.size.factors = TRUE, subset.row = NULL, ...):Normalizes counts by dividing by size factors.
size.factorsnumeric vector of cell-specific size factors
loglogical scalar indicating whether normalized values should be log2-transformed
transformstring specifying the transformation to apply to the normalized expression values.
pseudo.countnumeric scalar specifying the pseudo-count
to add when transform = "log"
center.size.factorslogical scalar indicating whether size factors should be centered at unity before being used
subset.rowvector specifying the subset of rows of x for which to return normalized values
calculateTPM(x, lengths = NULL, ...):Calculates transcripts per million using SQL-optimized row-wise sweep.
lengthsNumeric vector of gene lengths.
calculateCPM(x, size.factors = NULL, subset.row = NULL, ...):Calculates counts per million using DelayedArray-safe sweep operations.
size.factorsnumeric vector containing size factors to adjust the library sizes. If NULL, library sizes are used directly.
subset.rowvector specifying the subset of rows of x for which to return normalized values
calculateAverage(x, size.factors = NULL, subset.row = NULL, BPPARAM = SerialParam(), ...):Calculates average counts per feature using DelayedArray-safe sweep operations.
size.factorsnumeric vector containing size factors. If NULL, library size factors are computed automatically.
subset.rowvector specifying the subset of rows of x for which to return averages
BPPARAMBiocParallelParam object for parallel computation
The following normalization methods will dispatch to optimized
DuckDBMatrix implementations of normalizeCounts,
librarySizeFactors, calculateCPM, calculateAverage,
and calculateTPM.
The following aggregation methods have optimized DuckDBMatrix implementations that use SQL GROUP BY operations for efficient pseudo-bulk analysis:
numDetectedAcrossFeatures(x, ids, subset.row = NULL, subset.col = NULL,
average = FALSE, threshold = 0, ...):Count detected features across feature sets for each cell.
idsfactor or list specifying feature groupings
averagelogical indicating whether to compute the proportion
thresholdthreshold for detection
sumCountsAcrossFeatures(x, ids, subset.row = NULL, subset.col = NULL,
average = FALSE, ...):Sum counts across feature sets for each cell.
idsfactor or list specifying feature groupings
averagelogical indicating whether to compute the average
summarizeAssayByGroup(x, ids, subset.row = NULL, subset.col = NULL,
statistics = c("mean", "sum", "num.detected", "prop.detected"),
store.number = "ncells", threshold = 0, ...):From an assay matrix, compute summary statistics for groups of cells.
idsfactor specifying the group for each cell
statisticscharacter vector of statistics to compute
thresholdthreshold for detection
Patrick Aboyoun
DuckDBMatrix-class for the main class
DuckDBArray-matrixStats for the matrixStats methods
perCellQCMetrics for the scuttle generic
perFeatureQCMetrics for the scuttle generic
librarySizeFactors for the scuttle generic
normalizeCounts for the scuttle generic
calculateCPM for the scuttle generic
calculateAverage for the scuttle generic
calculateTPM for the scuttle generic
numDetectedAcrossFeatures for the scuttle generic
sumCountsAcrossFeatures for the scuttle generic
summarizeAssayByGroup for the scuttle generic
mat <- matrix(rpois(500, 5), 25, 20) dimnames(mat) <- list(paste0("G", 1:25), paste0("C", 1:20)) names(dimnames(mat)) <- c("index1", "index2") tmp <- tempfile() writeParquet(mat, tmp) pqmat <- DuckDBMatrix(tmp, datacol = "value", keycols = lapply(dimnames(mat), function(x) setNames(seq_along(x), x))) head(librarySizeFactors(pqmat)) unlink(tmp, recursive = TRUE)mat <- matrix(rpois(500, 5), 25, 20) dimnames(mat) <- list(paste0("G", 1:25), paste0("C", 1:20)) names(dimnames(mat)) <- c("index1", "index2") tmp <- tempfile() writeParquet(mat, tmp) pqmat <- DuckDBMatrix(tmp, datacol = "value", keycols = lapply(dimnames(mat), function(x) setNames(seq_along(x), x))) head(librarySizeFactors(pqmat)) unlink(tmp, recursive = TRUE)
Registers a initializeCpp method for
DuckDBArraySeed objects (the seed underlying every
DuckDBMatrix), giving compiled C++ code that goes through
beachmat/tatami (e.g. BiocSingular's
compute_center/compute_scale) a fast path instead of always
falling back to beachmat's generic "unknown matrix" block-processing
path.
## S4 method for signature 'DuckDBArraySeed' initializeCpp( x, ..., duckdb.realize = initializeOptions("realize"), duckdb.memory_limit = initializeOptions("memory_limit") )## S4 method for signature 'DuckDBArraySeed' initializeCpp( x, ..., duckdb.realize = initializeOptions("realize"), duckdb.memory_limit = initializeOptions("memory_limit") )
x |
A DuckDBArraySeed object. |
... |
Further arguments, passed to beachmat's generic fallback method when the fast path is not applicable. |
duckdb.realize |
Logical scalar, see the |
duckdb.memory_limit |
Numeric scalar, see the |
Unlike beachmat.hdf5/beachmat.tiledb, there is no dedicated
tatami_duckdb C++ library implementing genuine on-demand,
out-of-core reads. This method instead materializes the seed's underlying
COO table into an in-memory sparse matrix via a single bulk SQL pull
(loadIntoMemory), then delegates to beachmat's own
native initializeCpp,dgCMatrix-method. This mirrors
beachmat.hdf5's opt-in realize path, not its default
on-demand streaming path, and is only attempted for a zero-filled (sparse)
seed within initializeOptions's "memory_limit".
Unlike beachmat.hdf5/beachmat.tiledb, the materialized
pointer is not cached across calls with
checkMemoryCache. Verified empirically that doing
so is unsafe here: two DuckDBMatrix objects built from the
same underlying query (the common case, e.g. re-reading the same release
twice) render identical SQL and would hit the same cache entry, but
reusing that cached pointer across a later, logically separate computation
silently corrupted results. Every call re-materializes, which costs a
fraction of a second for a "memory_limit"-sized matrix, negligible
next to the speedup this fast path already provides.
This is purely a performance optimization for anything that already goes
through initializeCpp (e.g. BiocSingular::runSVD's
center/scale computation). It does not speed up
irlba's Lanczos iteration itself, which calls %*% directly
on the DuckDBMatrix object rather than through
initializeCpp; use the SQL-pushdown %*%/crossprod
methods in DuckDBArray for that, or materialize small-enough matrices
with loadIntoMemory and drive irlba directly.
Whenever the fast path is not applicable (a non-zero-filled seed, more
than 2 keyed dimensions, or an estimated size over "memory_limit"),
this method falls back to beachmat's existing generic path rather
than raising an error, so it can never make previously-working (if slower)
code stop working.
An external pointer that can be used in any tatami-compatible function.
Patrick Aboyoun
initializeCpp for the generic
loadIntoMemory to force materialization directly
initializeOptions for the tunable defaults
# A genuinely sparse 2x2 matrix: (r2, c2) is an implicit zero. df <- data.frame(row = c("r1", "r2", "r1"), col = c("c1", "c1", "c2"), value = c(1, 2, 3)) tf <- tempfile(fileext = ".parquet") on.exit(unlink(tf)) arrow::write_parquet(df, tf) pqmat <- DuckDBArray::DuckDBMatrix(tf, datacol = "value", keycols = list(row = c("r1", "r2"), col = c("c1", "c2"))) ptr <- beachmat::initializeCpp(pqmat@seed)# A genuinely sparse 2x2 matrix: (r2, c2) is an implicit zero. df <- data.frame(row = c("r1", "r2", "r1"), col = c("c1", "c1", "c2"), value = c(1, 2, 3)) tf <- tempfile(fileext = ".parquet") on.exit(unlink(tf)) arrow::write_parquet(df, tf) pqmat <- DuckDBArray::DuckDBMatrix(tf, datacol = "value", keycols = list(row = c("r1", "r2"), col = c("c1", "c2"))) ptr <- beachmat::initializeCpp(pqmat@seed)
Options for initializing DuckDB-backed matrices in
initializeCpp.
initializeOptions(option, value)initializeOptions(option, value)
option |
String specifying the name of the option. |
value |
Value of the option. |
The following options are supported:
realize, a logical scalar specifying whether to materialize
a DuckDBArraySeed's underlying COO table into an
in-memory sparse matrix with loadIntoMemory on every
initializeCpp call (unlike beachmat.hdf5/
beachmat.tiledb, this is deliberately not cached across
calls, see initializeCpp's details for why). Unlike
beachmat.hdf5/beachmat.tiledb (which default this to
FALSE because they have a genuine on-demand streaming backend),
this defaults to TRUE: BiocDuckDB has no native out-of-core
tatami backend, so setting this to FALSE falls back to
beachmat's generic "unknown matrix" block-processing path rather
than a faster streaming one.
memory_limit, a numeric scalar specifying the maximum
estimated size, in bytes, of the sparse matrix materialized by
loadIntoMemory. Above this limit, materialization is
refused so a genuinely out-of-core matrix fails loudly (and falls back
to the generic path, when reached via initializeCpp)
instead of risking an OOM.
If value is missing, the current setting of option is
returned. If value is supplied, it is used to set the option, and
the previous value of the option is invisibly returned.
Patrick Aboyoun
initializeOptions("memory_limit") old <- initializeOptions("memory_limit", 4 * 1024^3) initializeOptions("memory_limit") initializeOptions("memory_limit", old)initializeOptions("memory_limit") old <- initializeOptions("memory_limit", 4 * 1024^3) initializeOptions("memory_limit") initializeOptions("memory_limit", old)
Joins the observations recorded in spatialMap to the rows of the
spatial layer they reference, through the full junction key
(assay, colname, element_type, region,
instance_id). Resolves a single (element_type, region) layer
(inferred from the filtered spatialMap when unambiguous, else
specify); the join is pushed to DuckDB and returned as a lazy
DuckDBDataFrame with the assay
identity columns plus the layer's coordinate columns.
linkSpatialMap( mase, assay = NULL, element_type = NULL, region = NULL, conn = acquireDuckDBConn() )linkSpatialMap( mase, assay = NULL, element_type = NULL, region = NULL, conn = acquireDuckDBConn() )
mase |
|
assay, element_type, region
|
Optional filters selecting the spatialMap rows (and thus the single layer) to link. |
conn |
A DuckDB connection (default the shared BiocDuckDB connection). |
A lazy DuckDBDataFrame of linked observations.
mase <- readParquet(system.file("extdata", "spatial_mase", package = "BiocDuckDB")) linkSpatialMap(mase, assay = "assay1")mase <- readParquet(system.file("extdata", "spatial_mase", package = "BiocDuckDB")) linkSpatialMap(mase, assay = "assay1")
Load a DuckDBMatrix's (or its DuckDBArraySeed's)
underlying COO table into memory as an external pointer to a
tatami-compatible representation, via a single bulk SQL pull. This is
what initializeCpp calls internally when
duckdb.realize = TRUE (the default); call it directly when you want
the materialized pointer without going through the initializeCpp
generic. Each call re-materializes; the result is deliberately not cached
(see initializeCpp's details for why).
loadIntoMemory(x, memory_limit = initializeOptions("memory_limit"))loadIntoMemory(x, memory_limit = initializeOptions("memory_limit"))
x |
A DuckDBMatrix or DuckDBArraySeed object. |
memory_limit |
Numeric scalar, see the |
An external pointer that can be used in tatami-based functions.
Patrick Aboyoun
initializeCpp, initializeOptions
# A genuinely sparse 2x2 matrix: (r2, c2) is an implicit zero. df <- data.frame(row = c("r1", "r2", "r1"), col = c("c1", "c1", "c2"), value = c(1, 2, 3)) tf <- tempfile(fileext = ".parquet") on.exit(unlink(tf)) arrow::write_parquet(df, tf) pqmat <- DuckDBArray::DuckDBMatrix(tf, datacol = "value", keycols = list(row = c("r1", "r2"), col = c("c1", "c2"))) ptr <- loadIntoMemory(pqmat)# A genuinely sparse 2x2 matrix: (r2, c2) is an implicit zero. df <- data.frame(row = c("r1", "r2", "r1"), col = c("c1", "c1", "c2"), value = c(1, 2, 3)) tf <- tempfile(fileext = ".parquet") on.exit(unlink(tf)) arrow::write_parquet(df, tf) pqmat <- DuckDBArray::DuckDBMatrix(tf, datacol = "value", keycols = list(row = c("r1", "r2"), col = c("c1", "c2"))) ptr <- loadIntoMemory(pqmat)
Lazy MultiAssaySpatialExperiment methods that keep spatial layers as DuckDBDataFrame objects when possible. In-memory MASE defaults are used when no lazy layers are present.
readParquetForMASE() returns the parsed table — a lazy
DuckDBDataFrame for large files, or an in-memory object for small ones
— and readGeoParquetForMASE() returns a geometry-bearing object (an
sf data frame). annotateWithRegions(),
subsetByBoundingBox(), and subsetByPolygon() return a
MultiAssaySpatialExperiment
(annotated or spatially subset).
readParquetForMASE(file_path, col_select = NULL, ...):Files larger than 50 MB are read as lazy DuckDBDataFrame objects;
smaller files use the in-memory MultiAssaySpatialExperiment method.
readGeoParquetForMASE(file_path, ...):Uses readGeoParquet when DuckDBSpatial
is installed; otherwise falls back to sfarrow.
annotateWithRegions(x, points, shapes, ...):Annotate points with shape regions using lazy spatialMatch.
subsetByBoundingBox(x, xmin, xmax, ymin, ymax, ...):Subset by bounding box with lazy layer filtering when applicable.
subsetByPolygon(x, polygon, ...):Subset by polygon with lazy layer filtering when applicable.
readParquetForMASE,
readGeoParquetForMASE,
annotateWithRegions,
subsetByBoundingBox, and
subsetByPolygon.
# subsetByBoundingBox on a DuckDB-backed layer uses DuckDB's optional spatial # extension; probe for it so the example is skipped cleanly where the # extension cannot load (offline / firewalled repository / not installed). spatial_ok <- requireNamespace("MultiAssaySpatialExperiment", quietly = TRUE) && requireNamespace("sf", quietly = TRUE) && requireNamespace("DuckDBSpatial", quietly = TRUE) && tryCatch({ DBI::dbGetQuery(DuckDBDataFrame::acquireDuckDBConn(), "SELECT ST_Area(ST_GeomFromText('POLYGON((0 0,0 1,1 1,1 0,0 0))'))") TRUE }, error = function(e) FALSE) if (spatial_ok) { pts <- S4Vectors::DataFrame( x = 1:3, y = 1:3, instance_id = c("A", "B", "C")) mat <- matrix(1:9, 3, 3, dimnames = list(paste0("G", 1:3), c("A", "B", "C"))) mase <- MultiAssaySpatialExperiment::MultiAssaySpatialExperiment( experiments = MultiAssayExperiment::ExperimentList(rna = mat), colData = S4Vectors::DataFrame(row.names = "s1"), sampleMap = S4Vectors::DataFrame( assay = "rna", primary = "s1", colname = c("A", "B", "C")), points = MultiAssaySpatialExperiment::PointsLayerList(centroids = pts)) tmp <- tempfile() writeParquet(mase, tmp) mase2 <- readParquet(tmp) subsetByBoundingBox(mase2, xmin = 1, xmax = 3, ymin = 1, ymax = 3) unlink(tmp, recursive = TRUE) }# subsetByBoundingBox on a DuckDB-backed layer uses DuckDB's optional spatial # extension; probe for it so the example is skipped cleanly where the # extension cannot load (offline / firewalled repository / not installed). spatial_ok <- requireNamespace("MultiAssaySpatialExperiment", quietly = TRUE) && requireNamespace("sf", quietly = TRUE) && requireNamespace("DuckDBSpatial", quietly = TRUE) && tryCatch({ DBI::dbGetQuery(DuckDBDataFrame::acquireDuckDBConn(), "SELECT ST_Area(ST_GeomFromText('POLYGON((0 0,0 1,1 1,1 0,0 0))'))") TRUE }, error = function(e) FALSE) if (spatial_ok) { pts <- S4Vectors::DataFrame( x = 1:3, y = 1:3, instance_id = c("A", "B", "C")) mat <- matrix(1:9, 3, 3, dimnames = list(paste0("G", 1:3), c("A", "B", "C"))) mase <- MultiAssaySpatialExperiment::MultiAssaySpatialExperiment( experiments = MultiAssayExperiment::ExperimentList(rna = mat), colData = S4Vectors::DataFrame(row.names = "s1"), sampleMap = S4Vectors::DataFrame( assay = "rna", primary = "s1", colname = c("A", "B", "C")), points = MultiAssaySpatialExperiment::PointsLayerList(centroids = pts)) tmp <- tempfile() writeParquet(mase, tmp) mase2 <- readParquet(tmp) subsetByBoundingBox(mase2, xmin = 1, xmax = 3, ymin = 1, ymax = 3) unlink(tmp, recursive = TRUE) }
Reads a parquet representation of various Bioconductor objects from disk
using the Frictionless Data Package metadata model, reconstructing them as
DuckDB-backed objects. This function is the counterpart to
writeParquet and supports reading SummarizedExperiment,
RangedSummarizedExperiment, SingleCellExperiment, and
MultiAssayExperiment objects that have been written to parquet format
with comprehensive metadata.
readParquet( path, package = .readDatapackage(path), model = package[["model"]], ... )readParquet( path, package = .readDatapackage(path), model = package[["model"]], ... )
path |
A character string specifying the path to the directory
containing the parquet files. The directory must contain a
|
package |
A list containing the parsed |
model |
A character string identifying the package schema — which
container class to reconstruct. Defaults to |
... |
Additional arguments passed to internal helper functions. |
This function reads parquet files that were created by
writeParquet using the Frictionless Data Package metadata model
and reconstructs the original Bioconductor object structure. This function
supports different object types through a switch statement that routes to
appropriate helper functions:
SummarizedExperiment objects: Reads feature data, sample data,
and assay data from separate parquet files, reconstructing the object with
DuckDB-backed components. For ranged experiments, genomic coordinates are
reconstructed as DuckDBGRanges objects.
SingleCellExperiment objects: Extends SummarizedExperiment
functionality by also reading reduced dimensions, loadings, alternative
experiments, row / column tables, and row / column pairings from their
respective parquet files.
MultiAssayExperiment objects: Reads multiple experiments
along with sample data and sample mapping information, reconstructing the
multi-experiment structure.
This function uses the Frictionless Data Package metadata to understand the
structure of the data, including resource schemas with field definitions,
primary keys, and semantic meanings (sequence_name, start, end, strand for
genomic data) encoded in the datapackage.json file.
A Bioconductor object of the appropriate class, reconstructed using DuckDB backend classes:
DuckDBMatrix objects for assay data
DuckDBDataFrame objects for row and column metadata
DuckDBGRanges objects for genomic ranges
DuckDBGRangesList objects for grouped genomic ranges
DuckDBSelfHits objects for graph edge lists
DuckDBDualSubset objects for pairwise graphs (wrapping DuckDBSelfHits)
GenomicRangesRead with genomic coordinates (seqnames, start,
end, strand) and optional metadata columns.
GenomicRangesListRead with genomic coordinates and metadata columns.
DuckDBSelfHitsGraph edge lists with from, to columns and optional
metadata. Node count (nnode) is stored in the schema
graphEdges property.
SummarizedExperimentFeature metadata from features/, sample metadata from
samples/, and assays from flat assay_<name>/ directories.
Any complex objects stored in metadata() are written to
unbound_<name>/ directories and restored on read. When the
assay is hive-partitioned, features/ and samples/ each
carry a matching __feature__group__/__sample__group__
column (a dimtbl, from the same grid the assay is partitioned
by) so a reader can prune the dimension tables in step with the fact
table. The assay's own partition nesting is
__sample__group__=.../__feature__group__=... (sample outer,
feature inner) – see ?writeParquet's Assays objects
section for why.
RangedSummarizedExperimentAs SummarizedExperiment, with rowRanges reconstructed as a
DuckDBGRanges from features/.
SingleCellExperimentExtends SummarizedExperiment with: reduced dimensions from
sample_embeddings/; row loadings from feature_embeddings/;
alternative experiments from experiment_<name>/ directories;
row/column tables from flat feature_table_<name>/ and
sample_table_<name>/ directories; row/column pairwise graphs from
flat feature_graph_<name>/ and sample_graph_<name>/
directories. Unlike features//samples/,
sample_embeddings/ and feature_embeddings/ never carry a
__sample__group__/__feature__group__ dimtbl
column – that partitioning-alignment metadata lives on the dimension
tables alone, not duplicated onto the embeddings.
MultiAssayExperimentExperiments written directly to root as experiment_<name>/
directories (flattened). Each SummarizedExperiment has its own
datapackage.json (layout = "nested_experiment").
Also includes subjects (subject metadata) and sample_map
(subject-to-sample mapping) as unbound resources.
MultiAssaySpatialExperimentExtends MultiAssayExperiment with spatial points from flat
sample_points_<name>/ directories, shapes from flat
sample_shapes_<name>/ directories, image metadata from
img_data/, and spatial mapping from spatial_map/.
Requires the MultiAssaySpatialExperiment package.
This function reads data packages that follow the Frictionless Data Package
specification (extended by the BiocDuckDB profile), parsing the
datapackage.json metadata file. Dispatch operates at two levels:
Package level (root datapackage.json):
model — which container class to reconstruct; absent means
SimpleList fallback.
annotations — non-relational metadata from metadata(x)
(scalars, vectors, 1-D arrays, JSON-safe nested lists). Tabular items
are referenced via parquet_ref stubs pointing at unbound
Parquet sidecars; mixed nested lists use a nested_mapping wrapper.
Resource level (entries in resources array):
dimension — biological axis ("feature",
"sample", "crossed", "unbound").
layout — physical storage pattern (e.g.,
"data_frame", "coord_array", "graph_edges",
"nested_experiment"); routes each resource to the
appropriate low-level reader via .readParquetResource.
schema — field definitions, primary keys, and BiocDuckDB
annotations (genomicCoords, graphEdges, arrayItem).
Patrick Aboyoun
writeParquet for writing Bioconductor objects to parquet
DuckDBMatrix for matrix storage
DuckDBDataFrame for metadata storage
DuckDBGRanges for genomic ranges
DuckDBGRangesList for grouped genomic ranges
DuckDBSelfHits for graph edge lists
se <- SummarizedExperiment::SummarizedExperiment( assays = list(counts = matrix(rpois(500, 5), 25, 20)), colData = S4Vectors::DataFrame(sample = rep(c("A", "B"), each = 10))) tmpdir <- tempfile() writeParquet(se, tmpdir) se2 <- readParquet(tmpdir) class(se2) dim(se2) unlink(tmpdir, recursive = TRUE)se <- SummarizedExperiment::SummarizedExperiment( assays = list(counts = matrix(rpois(500, 5), 25, 20)), colData = S4Vectors::DataFrame(sample = rep(c("A", "B"), each = 10))) tmpdir <- tempfile() writeParquet(se, tmpdir) se2 <- readParquet(tmpdir) class(se2) dim(se2) unlink(tmpdir, recursive = TRUE)
Methods to get or set nested sample-level tables in a
SingleCellExperiment
object. These tables enable storage of multi-column relational data
associated with samples (cells/perturbations), which would otherwise require
nested DataFrames in colData(). By extracting nested tables into a
separate slot, they can be serialized to independent Parquet tables for
efficient querying.
x |
A SingleCellExperiment object. |
type |
String or integer scalar specifying the name or index of the table to get or set. |
withDimnames |
Logical scalar indicating whether row names should be
extracted from (getters) or set to (setters) the column names of |
... |
Additional arguments, currently ignored. |
value |
For the getter, a DataFrame-like object with number of rows
equal to |
Sample tables (colTables) are stored in
int_colData(x)$colTables and provide a mechanism to unnest complex
relational data from colData(). This is particularly useful when
sample metadata contains multi-valued properties or hierarchical structures
that are difficult to query when embedded as nested DataFrames.
Common use cases include:
Patient metadata: multiple disease diagnoses, treatment histories, or demographic details per sample
Experimental metadata: replicate-level measurements, batch details, or quality control metrics
Cell lineage: developmental trajectories or cell state transitions
Each table is a DataFrame with
ncol(x) rows, maintaining alignment with samples. Tables are
automatically subset when the parent SingleCellExperiment is subset by
columns.
For colTable, a DataFrame containing sample-level relational data.
For colTables, a SimpleList of such DataFrames.
For colTableNames, a character vector of table names.
For all setters, x is returned with the modified tables or names.
In the following examples, x is a
SingleCellExperiment
object.
colTable(x, type, withDimnames=TRUE):Retrieves a DataFrame containing sample-level relational data. type
is either a string specifying the name of the table in x to retrieve,
or a numeric scalar specifying the index of the desired table.
If withDimnames=TRUE, row names of the output DataFrame are replaced
with the column names of x.
colTableNames(x):Returns a character vector containing the names of all tables in x.
This is guaranteed to be of the same length as the number of tables, though
the names may not be unique.
colTables(x, withDimnames=TRUE):Returns a named SimpleList of DataFrames
containing one or more tables. Each table has the same number of rows as
ncol(x).
If withDimnames=TRUE, row names of each DataFrame are replaced with
the column names of x.
colTable(x, type, withDimnames=TRUE) <- value will add or replace a
table in a
SingleCellExperiment
object x. The value of type determines how the table is added
or replaced:
If type is a numeric scalar, it must be within the range of
existing tables, and value will be assigned to the table at that
index.
If type is a string and a table exists with this name,
value is assigned to that table. Otherwise a new table with this
name is appended to the existing list of tables.
value is expected to be a DataFrame or data.frame-like object with
number of rows equal to ncol(x).
If withDimnames=TRUE, row names of value are set to
colnames(x).
In the following examples, x is a
SingleCellExperiment
object.
colTables(x, withDimnames=TRUE) <- value:Replaces all tables in x with those in value.
The latter should be a list-like object containing any number of DataFrames
or data.frame-like objects with number of rows equal to ncol(x).
If value is named, those names will be used to name the tables in
x.
If value is a Annotated object, any
metadata will be retained in colTables(x).
If value is a Vector object, any
mcols will also be retained.
If withDimnames=TRUE, row names in each entry of value are set
to colnames(x).
colTableNames(x) <- value:Replaces all names for tables in x with a character vector
value. This should be of length equal to the number of tables
currently in x.
Patrick Aboyoun
colData for simple sample metadata.
rowTables for feature-level nested tables.
int_colData for the internal column
metadata storage.
library(SingleCellExperiment) # Create example SCE sce <- SingleCellExperiment( assays = list(counts = matrix(rpois(1000, 5), 100, 10)) ) rownames(sce) <- paste0("Gene", 1:100) colnames(sce) <- paste0("Cell", 1:10) # Add a table of patient disease history (multiple diseases per patient) diseases <- DataFrame( disease_id = paste0("MONDO:", sample(100000:999999, 10)), disease_label = paste0("Disease_", 1:10), onset_age = sample(20:80, 10, replace = TRUE) ) colTable(sce, "diseases") <- diseases # Add a table of per-sample QC metrics qc_metrics <- DataFrame( metric_name = rep(c("n_genes", "n_counts", "pct_mito"), length.out = 10), value = runif(10, 1000, 5000), passed = sample(c(TRUE, FALSE), 10, replace = TRUE) ) colTable(sce, "qc") <- qc_metrics # Retrieve all tables all_tables <- colTables(sce) names(all_tables) # Retrieve specific table by name dis <- colTable(sce, "diseases") dim(dis) # 10 samples × 3 columns # Retrieve by index qc <- colTable(sce, 2) # Get table names colTableNames(sce) # Subset SCE - tables are automatically subset sce_sub <- sce[, 1:5] dim(colTable(sce_sub, "diseases")) # 5 samples × 3 columnslibrary(SingleCellExperiment) # Create example SCE sce <- SingleCellExperiment( assays = list(counts = matrix(rpois(1000, 5), 100, 10)) ) rownames(sce) <- paste0("Gene", 1:100) colnames(sce) <- paste0("Cell", 1:10) # Add a table of patient disease history (multiple diseases per patient) diseases <- DataFrame( disease_id = paste0("MONDO:", sample(100000:999999, 10)), disease_label = paste0("Disease_", 1:10), onset_age = sample(20:80, 10, replace = TRUE) ) colTable(sce, "diseases") <- diseases # Add a table of per-sample QC metrics qc_metrics <- DataFrame( metric_name = rep(c("n_genes", "n_counts", "pct_mito"), length.out = 10), value = runif(10, 1000, 5000), passed = sample(c(TRUE, FALSE), 10, replace = TRUE) ) colTable(sce, "qc") <- qc_metrics # Retrieve all tables all_tables <- colTables(sce) names(all_tables) # Retrieve specific table by name dis <- colTable(sce, "diseases") dim(dis) # 10 samples × 3 columns # Retrieve by index qc <- colTable(sce, 2) # Get table names colTableNames(sce) # Subset SCE - tables are automatically subset sce_sub <- sce[, 1:5] dim(colTable(sce_sub, "diseases")) # 5 samples × 3 columns
Methods to get or set feature-level loading matrices in a SingleCellExperiment object. These matrices represent per-feature contributions to latent factors, embeddings, or statistical models, where feature identity (e.g., gene names) carries semantic meaning. Each row of a loading matrix corresponds to a row (feature) of the SingleCellExperiment object.
x |
A SingleCellExperiment object. |
type |
String or integer scalar specifying the name or index of the loading result to get or set. |
withDimnames |
Logical scalar indicating whether row names should be
extracted from (getters) or set to (setters) the row names of |
... |
Additional arguments, currently ignored. |
value |
For the getter, a matrix-like object with number of rows equal
to |
Feature loadings (rowLoadings) are stored in
int_elementMetadata(x)$rowLoadings and represent the AnnData
.varm equivalent in R. Unlike sample embeddings (reducedDims)
where dimension labels are arbitrary coordinates, loading matrices have
meaningful row names that identify which features contribute to each
latent dimension.
Common use cases include:
PCA loadings: which genes load on each principal component
Factor analysis: gene weights for latent factors
Statistical summaries: per-gene effect sizes or log fold changes across conditions
Loading matrices are automatically subset when the parent SingleCellExperiment is subset by rows, maintaining alignment between features and their loadings.
For rowLoading, a matrix containing loading values for features (rows)
and dimensions (columns).
For rowLoadings, a SimpleList of such matrices.
For rowLoadingNames, a character vector of loading names.
For all setters, x is returned with the modified loading results or
names.
In the following examples, x is a
SingleCellExperiment
object.
rowLoading(x, type, withDimnames=TRUE):Retrieves a matrix containing feature loadings (rows) for each latent
dimension (columns). type is either a string specifying the name of
the loading result in x to retrieve, or a numeric scalar specifying
the index of the desired result.
If withDimnames=TRUE, row names of the output matrix are replaced with
the row names of x.
rowLoadingNames(x):Returns a character vector containing the names of all loading results in
x. This is guaranteed to be of the same length as the number of
results, though the names may not be unique.
rowLoadings(x, withDimnames=TRUE):Returns a named SimpleList of matrices
containing one or more loading results. Each result is a matrix with the same
number of rows as nrow(x).
If withDimnames=TRUE, row names of each matrix are replaced with the
row names of x.
rowLoading(x, type, withDimnames=TRUE) <- value will add or replace a
loading result in a
SingleCellExperiment
object x. The value of type determines how the result is added
or replaced:
If type is a numeric scalar, it must be within the range of
existing results, and value will be assigned to the result at that
index.
If type is a string and a result exists with this name,
value is assigned to that result. Otherwise a new result with this
name is appended to the existing list of results.
value is expected to be a matrix or matrix-like object with number of
rows equal to nrow(x).
If withDimnames=TRUE, row names of value are set to
rownames(x).
In the following examples, x is a
SingleCellExperiment
object.
rowLoadings(x, withDimnames=TRUE) <- value:Replaces all loading results in x with those in value.
The latter should be a list-like object containing any number of matrices or
matrix-like objects with number of rows equal to nrow(x).
If value is named, those names will be used to name the loading
results in x.
If value is a Annotated object, any
metadata will be retained in rowLoadings(x).
If value is a Vector object, any
mcols will also be retained.
If withDimnames=TRUE, row names in each entry of value are set
to rownames(x).
rowLoadingNames(x) <- value:Replaces all names for loading results in x with a character vector
value. This should be of length equal to the number of results
currently in x.
Patrick Aboyoun
reducedDims for sample-level embeddings
(observation matrices).
int_elementMetadata for the internal row
metadata storage.
library(SingleCellExperiment) # Create example SCE with perturbation data sce <- SingleCellExperiment( assays = list(beta_Z = matrix(rnorm(1000), 100, 10)) ) rownames(sce) <- paste0("Gene", 1:100) colnames(sce) <- paste0("Pert", 1:10) # Add PCA loadings (genes × components) pca_loadings <- matrix(rnorm(100 * 5), 100, 5) rownames(pca_loadings) <- rownames(sce) colnames(pca_loadings) <- paste0("PC", 1:5) rowLoading(sce, "PCA") <- pca_loadings # Add factor analysis loadings (genes × factors) factor_loadings <- matrix(rnorm(100 * 20), 100, 20) rownames(factor_loadings) <- rownames(sce) colnames(factor_loadings) <- paste0("Factor", 1:20) rowLoading(sce, "FactorAnalysis") <- factor_loadings # Retrieve all loadings all_loadings <- rowLoadings(sce) names(all_loadings) # Retrieve specific loading by name pca <- rowLoading(sce, "PCA") dim(pca) # 100 genes × 5 components # Retrieve by index factors <- rowLoading(sce, 2) # Get loading names rowLoadingNames(sce) # Subset SCE - loadings are automatically subset sce_sub <- sce[1:50, ] dim(rowLoading(sce_sub, "PCA")) # 50 genes × 5 componentslibrary(SingleCellExperiment) # Create example SCE with perturbation data sce <- SingleCellExperiment( assays = list(beta_Z = matrix(rnorm(1000), 100, 10)) ) rownames(sce) <- paste0("Gene", 1:100) colnames(sce) <- paste0("Pert", 1:10) # Add PCA loadings (genes × components) pca_loadings <- matrix(rnorm(100 * 5), 100, 5) rownames(pca_loadings) <- rownames(sce) colnames(pca_loadings) <- paste0("PC", 1:5) rowLoading(sce, "PCA") <- pca_loadings # Add factor analysis loadings (genes × factors) factor_loadings <- matrix(rnorm(100 * 20), 100, 20) rownames(factor_loadings) <- rownames(sce) colnames(factor_loadings) <- paste0("Factor", 1:20) rowLoading(sce, "FactorAnalysis") <- factor_loadings # Retrieve all loadings all_loadings <- rowLoadings(sce) names(all_loadings) # Retrieve specific loading by name pca <- rowLoading(sce, "PCA") dim(pca) # 100 genes × 5 components # Retrieve by index factors <- rowLoading(sce, 2) # Get loading names rowLoadingNames(sce) # Subset SCE - loadings are automatically subset sce_sub <- sce[1:50, ] dim(rowLoading(sce_sub, "PCA")) # 50 genes × 5 components
Specialized setter methods for colPairs and rowPairs that
automatically wrap DuckDBSelfHits
objects in DuckDBDualSubset containers. This enables lazy
node-based subsetting when a SingleCellExperiment is subset.
x |
A SingleCellExperiment object. |
type |
String or integer scalar specifying the name or index of the pairing to replace. |
... |
Additional arguments passed to the default setter. |
value |
A DuckDBSelfHits object to be stored as a column/row pairing. This will be automatically wrapped in a DuckDBDualSubset. |
These methods extend the default colPairs<- and rowPairs<-
setters from SingleCellExperiment to recognize DuckDBSelfHits objects
and wrap them in DuckDBDualSubset containers. This ensures that when
a SingleCellExperiment is subset (e.g., sce[, 1:100]), the wrapped
DuckDBSelfHits objects are lazily filtered via SQL rather than
materialized in memory.
For the setter methods, x is returned with the modified pairings.
# Create SCE with lazy KNN graph
library(SingleCellExperiment)
library(BiocDuckDB)
sce <- SingleCellExperiment(assays = list(counts = matrix(1:100, 10, 10)))
# Create DuckDBSelfHits from parquet
knn_hits <- DuckDBSelfHits("knn_edges.parquet",
from = "from",
to = "to",
nnode = ncol(sce))
# Assign to colPairs (auto-wraps in DuckDBDualSubset)
colPairs(sce, "knn") <- knn_hits
# Subsetting SCE automatically subsets the graph lazily
sce_sub <- sce[, 1:5]
pairs_sub <- colPairs(sce_sub, "knn") # Lazy filtered to nodes 1:5
Patrick Aboyoun
DuckDBSelfHits for the lazy edge storage class.
DuckDBDualSubset for the wrapper that enables node-based subsetting.
colPairs and
rowPairs for the base SingleCellExperiment methods.
library(SingleCellExperiment) sce <- SingleCellExperiment(assays = list(counts = matrix(1:50, 10, 5))) hits <- S4Vectors::SelfHits( from = rep(1:5, each = 2), to = sample(1:5, 10, replace = TRUE), nnode = 5L) tmp <- tempfile() writeParquet(hits, tmp) ddb_hits <- DuckDBSelfHits(tmp, from = "from", to = "to", nnode = 5L) colPair(sce, "knn") <- ddb_hits class(colPair(sce, "knn")) unlink(tmp, recursive = TRUE)library(SingleCellExperiment) sce <- SingleCellExperiment(assays = list(counts = matrix(1:50, 10, 5))) hits <- S4Vectors::SelfHits( from = rep(1:5, each = 2), to = sample(1:5, 10, replace = TRUE), nnode = 5L) tmp <- tempfile() writeParquet(hits, tmp) ddb_hits <- DuckDBSelfHits(tmp, from = "from", to = "to", nnode = 5L) colPair(sce, "knn") <- ddb_hits class(colPair(sce, "knn")) unlink(tmp, recursive = TRUE)
Methods to get or set nested feature-level tables in a
SingleCellExperiment
object. These tables enable storage of multi-column relational data
associated with features (genes), which would otherwise require nested
DataFrames in rowData(). By extracting nested tables into a separate
slot, they can be serialized to independent Parquet tables for efficient
querying.
x |
A SingleCellExperiment object. |
type |
String or integer scalar specifying the name or index of the table to get or set. |
withDimnames |
Logical scalar indicating whether row names should be
extracted from (getters) or set to (setters) the row names of |
... |
Additional arguments, currently ignored. |
value |
For the getter, a DataFrame-like object with number of rows
equal to |
Feature tables (rowTables) are stored in
int_elementMetadata(x)$rowTables and provide a mechanism to unnest
complex relational data from rowData(). This is particularly useful
when feature metadata contains multi-valued properties or hierarchical
structures that are difficult to query when embedded as nested DataFrames.
Common use cases include:
Gene annotation tables: transcript isoforms, protein domains, or GO terms
Experimental design tables: per-gene treatment conditions or batch effects
Statistical results: detailed per-gene statistics across multiple contrasts
Each table is a DataFrame with
nrow(x) rows, maintaining alignment with features. Tables are
automatically subset when the parent SingleCellExperiment is subset by rows.
For rowTable, a DataFrame containing feature-level relational data.
For rowTables, a SimpleList of such DataFrames.
For rowTableNames, a character vector of table names.
For all setters, x is returned with the modified tables or names.
In the following examples, x is a
SingleCellExperiment
object.
rowTable(x, type, withDimnames=TRUE):Retrieves a DataFrame containing feature-level relational data. type
is either a string specifying the name of the table in x to retrieve,
or a numeric scalar specifying the index of the desired table.
If withDimnames=TRUE, row names of the output DataFrame are replaced
with the row names of x.
rowTableNames(x):Returns a character vector containing the names of all tables in x.
This is guaranteed to be of the same length as the number of tables, though
the names may not be unique.
rowTables(x, withDimnames=TRUE):Returns a named SimpleList of DataFrames
containing one or more tables. Each table has the same number of rows as
nrow(x).
If withDimnames=TRUE, row names of each DataFrame are replaced with
the row names of x.
rowTable(x, type, withDimnames=TRUE) <- value will add or replace a
table in a
SingleCellExperiment
object x. The value of type determines how the table is added
or replaced:
If type is a numeric scalar, it must be within the range of
existing tables, and value will be assigned to the table at that
index.
If type is a string and a table exists with this name,
value is assigned to that table. Otherwise a new table with this
name is appended to the existing list of tables.
value is expected to be a DataFrame or data.frame-like object with
number of rows equal to nrow(x).
If withDimnames=TRUE, row names of value are set to
rownames(x).
In the following examples, x is a
SingleCellExperiment
object.
rowTables(x, withDimnames=TRUE) <- value:Replaces all tables in x with those in value.
The latter should be a list-like object containing any number of DataFrames
or data.frame-like objects with number of rows equal to nrow(x).
If value is named, those names will be used to name the tables in
x.
If value is a Annotated object, any
metadata will be retained in rowTables(x).
If value is a Vector object, any
mcols will also be retained.
If withDimnames=TRUE, row names in each entry of value are set
to rownames(x).
rowTableNames(x) <- value:Replaces all names for tables in x with a character vector
value. This should be of length equal to the number of tables
currently in x.
Patrick Aboyoun
rowData for simple feature metadata.
colTables for sample-level nested tables.
int_elementMetadata for the internal row
metadata storage.
library(SingleCellExperiment) # Create example SCE sce <- SingleCellExperiment( assays = list(counts = matrix(rpois(1000, 5), 100, 10)) ) rownames(sce) <- paste0("Gene", 1:100) colnames(sce) <- paste0("Cell", 1:10) # Add a table of gene isoforms (multiple isoforms per gene) isoforms <- DataFrame( isoform_id = paste0("ISO", 1:100), length = sample(500:5000, 100), is_canonical = sample(c(TRUE, FALSE), 100, replace = TRUE) ) rowTable(sce, "isoforms") <- isoforms # Add a table of differential expression results across conditions de_results <- DataFrame( condition = rep(c("treated", "control"), each = 50), log2fc = rnorm(100), pvalue = runif(100) ) rowTable(sce, "de_stats") <- de_results # Retrieve all tables all_tables <- rowTables(sce) names(all_tables) # Retrieve specific table by name iso <- rowTable(sce, "isoforms") dim(iso) # 100 genes × 3 columns # Retrieve by index de <- rowTable(sce, 2) # Get table names rowTableNames(sce) # Subset SCE - tables are automatically subset sce_sub <- sce[1:50, ] dim(rowTable(sce_sub, "isoforms")) # 50 genes × 3 columnslibrary(SingleCellExperiment) # Create example SCE sce <- SingleCellExperiment( assays = list(counts = matrix(rpois(1000, 5), 100, 10)) ) rownames(sce) <- paste0("Gene", 1:100) colnames(sce) <- paste0("Cell", 1:10) # Add a table of gene isoforms (multiple isoforms per gene) isoforms <- DataFrame( isoform_id = paste0("ISO", 1:100), length = sample(500:5000, 100), is_canonical = sample(c(TRUE, FALSE), 100, replace = TRUE) ) rowTable(sce, "isoforms") <- isoforms # Add a table of differential expression results across conditions de_results <- DataFrame( condition = rep(c("treated", "control"), each = 50), log2fc = rnorm(100), pvalue = runif(100) ) rowTable(sce, "de_stats") <- de_results # Retrieve all tables all_tables <- rowTables(sce) names(all_tables) # Retrieve specific table by name iso <- rowTable(sce, "isoforms") dim(iso) # 100 genes × 3 columns # Retrieve by index de <- rowTable(sce, 2) # Get table names rowTableNames(sce) # Subset SCE - tables are automatically subset sce_sub <- sce[1:50, ] dim(rowTable(sce_sub, "isoforms")) # 50 genes × 3 columns
Returns the names of the coordinate systems referenced by the per-element
transforms stored in metadata(x)$transforms (the targets each element
is mapped into). Empty when no transforms are recorded.
spatialCoordinateSystems(x)spatialCoordinateSystems(x)
x |
A character vector of coordinate-system names.
mase <- readParquet(system.file("extdata", "spatial_mase", package = "BiocDuckDB")) spatialCoordinateSystems(mase)mase <- readParquet(system.file("extdata", "spatial_mase", package = "BiocDuckDB")) spatialCoordinateSystems(mase)
Joins a points element to a shapes element (each named
"<element_type>/<region>") by a spatial predicate, pushed to DuckDB
via DuckDBSpatial. When coordinate_system is given and the MASE
carries transforms, each element is first aligned into that coordinate system
through the coordinate-transform graph
(spatialCoordinateSystems), so elements defined in different
intrinsic frames are compared correctly. This generalizes
annotateWithRegions (point-in-polygon with the default predicate
only) to any sf binary predicate.
spatialElementJoin( mase, x, y, join = sf::st_intersects, coordinate_system = NULL, x_col = "x", y_col = "y" )spatialElementJoin( mase, x, y, join = sf::st_intersects, coordinate_system = NULL, x_col = "x", y_col = "y" )
mase |
|
x |
A points element spec, |
y |
A shapes element spec, |
join |
A spatial predicate function (default
|
coordinate_system |
Optional target coordinate-system name to align both elements into before joining. |
x_col, y_col
|
Coordinate column names in |
The spatial-join result from DuckDBSpatial
(spatialMatch indices matching
each point in x to a row of y, NA where unmatched).
pts <- S4Vectors::DataFrame(x = c(1.5, 2.5, 3.5), y = c(1.5, 2.5, 3.5), instance_id = c("A", "B", "C")) shp <- S4Vectors::DataFrame(instance_id = c("cell1", "cell2", "cell3"), geometry = sf::st_sfc( sf::st_polygon(list(matrix(c(1,1,2,1,2,2,1,2,1,1), ncol = 2, byrow = TRUE))), sf::st_polygon(list(matrix(c(2,1,3,1,3,2,2,2,2,1), ncol = 2, byrow = TRUE))), sf::st_polygon(list(matrix(c(2,2,3,2,3,3,2,3,2,2), ncol = 2, byrow = TRUE))))) mase <- MultiAssaySpatialExperiment::MultiAssaySpatialExperiment( experiments = MultiAssayExperiment::ExperimentList(assay1 = matrix(1:9, 3, 3, dimnames = list(paste0("G", 1:3), c("A", "B", "C")))), colData = S4Vectors::DataFrame(row.names = "s1"), sampleMap = S4Vectors::DataFrame(assay = "assay1", primary = "s1", colname = c("A", "B", "C")), points = MultiAssaySpatialExperiment::PointsLayerList(centroids = pts), shapes = MultiAssaySpatialExperiment::ShapesLayerList(cells = shp)) spatialElementJoin(mase, "points/centroids", "shapes/cells")pts <- S4Vectors::DataFrame(x = c(1.5, 2.5, 3.5), y = c(1.5, 2.5, 3.5), instance_id = c("A", "B", "C")) shp <- S4Vectors::DataFrame(instance_id = c("cell1", "cell2", "cell3"), geometry = sf::st_sfc( sf::st_polygon(list(matrix(c(1,1,2,1,2,2,1,2,1,1), ncol = 2, byrow = TRUE))), sf::st_polygon(list(matrix(c(2,1,3,1,3,2,2,2,2,1), ncol = 2, byrow = TRUE))), sf::st_polygon(list(matrix(c(2,2,3,2,3,3,2,3,2,2), ncol = 2, byrow = TRUE))))) mase <- MultiAssaySpatialExperiment::MultiAssaySpatialExperiment( experiments = MultiAssayExperiment::ExperimentList(assay1 = matrix(1:9, 3, 3, dimnames = list(paste0("G", 1:3), c("A", "B", "C")))), colData = S4Vectors::DataFrame(row.names = "s1"), sampleMap = S4Vectors::DataFrame(assay = "assay1", primary = "s1", colname = c("A", "B", "C")), points = MultiAssaySpatialExperiment::PointsLayerList(centroids = pts), shapes = MultiAssaySpatialExperiment::ShapesLayerList(cells = shp)) spatialElementJoin(mase, "points/centroids", "shapes/cells")
Registers each spatial layer (points, shapes) and the spatialMap
junction of a
MultiAssaySpatialExperiment
as DuckDB temp views on a shared connection, so cross-element queries can be
expressed as SQL. Lazy
(DuckDBDataFrame) layers are
registered as views over their rendered SQL (no materialization); in-memory
layers and the materialized spatialMap are registered as temp tables.
This is the substrate used by linkSpatialMap /
validateSpatialMap and a handle for power-user raw SQL.
spatialViews(mase, conn = acquireDuckDBConn(), prefix = "mase_")spatialViews(mase, conn = acquireDuckDBConn(), prefix = "mase_")
mase |
|
conn |
A DuckDB connection (default the shared BiocDuckDB connection). |
prefix |
View-name prefix. |
A MASESpatialViews registry (a list of view names + conn).
mase <- readParquet(system.file("extdata", "spatial_mase", package = "BiocDuckDB")) v <- spatialViews(mase) v$spatial_mapmase <- readParquet(system.file("extdata", "spatial_mase", package = "BiocDuckDB")) v <- spatialViews(mase) v$spatial_map
Checks the spatialMap foreign keys against the spatial layers and the
assays, out-of-core via DuckDB anti-joins on lazy layers. Reports four
violation classes: unknown_layer (a (element_type, region) with
no matching layer), orphan_instance (a spatialMap row whose
instance_id is absent from its layer), orphan_colname (an
(assay, colname) that is not a column of that experiment), and
duplicate_instance (an instance_id occurring more than once
within a layer).
validateSpatialMap(mase, strict = FALSE, conn = acquireDuckDBConn())validateSpatialMap(mase, strict = FALSE, conn = acquireDuckDBConn())
mase |
|
strict |
If |
conn |
A DuckDB connection (default the shared BiocDuckDB connection). |
A DataFrame of violations (type, assay,
colname, element_type, region, instance_id,
detail); empty when the spatialMap is valid.
mase <- readParquet(system.file("extdata", "spatial_mase", package = "BiocDuckDB")) validateSpatialMap(mase) # empty report: the spatialMap is validmase <- readParquet(system.file("extdata", "spatial_mase", package = "BiocDuckDB")) validateSpatialMap(mase) # empty report: the spatialMap is valid
Assembles and writes the top-level datapackage.json manifest from a
list of already-written Frictionless resource descriptors. This is the
envelope step shared by the experiment-level writeParquet
methods, exposed so that producers who accumulate resources incrementally
(streaming large datasets, or promoting from another store) can emit a
conformant manifest without reconstructing an in-memory Bioconductor object.
writeDatapackage( model, resources, path, main_exp_name = NULL, annotations = NULL )writeDatapackage( model, resources, path, main_exp_name = NULL, annotations = NULL )
model |
Character(1) package-level schema identifier that selects the
|
resources |
A list of Frictionless resource descriptors (each a list),
as returned/accumulated from |
path |
Character(1) directory to write |
main_exp_name |
Optional character(1) naming the main experiment (used by
the |
annotations |
Optional list of non-relational metadata elements (as
produced during metadata serialization). Omitted when |
Each entry of resources is a Frictionless resource descriptor – a list
with name, path, dimension, layout, format,
mediatype, and schema – exactly as returned by the primitive
writeParquet methods (array, data.frame, DataFrame,
SelfHits). NULL entries are dropped, so the NULL returned
by append/streaming parts (see writeParquet) can be accumulated
and passed straight through. Descriptors are written verbatim otherwise; strip
any private, non-Frictionless keys before calling.
Invisibly, the assembled package list that was written.
Patrick Aboyoun
writeParquet for writing resources,
writeStreamingResource for streaming a flat resource from a
block-producing callback when accumulating descriptors piecemeal, and
readParquet for reading a written package (the
DuckDBMatrix/DuckDBArray/DuckDBTable constructors attach
an existing coord-array in place).
# Assemble a manifest from a hand-built resource descriptor. tf <- tempfile() resources <- list(list( name = "features", path = "features", dimension = "feature", layout = "data_frame", format = "parquet", mediatype = "application/vnd.apache.parquet", schema = list(fields = list(list(name = "id", type = "integer"))))) writeDatapackage("summarized_experiment", resources, tf) cat(readLines(file.path(tf, "datapackage.json")), sep = "\n")# Assemble a manifest from a hand-built resource descriptor. tf <- tempfile() resources <- list(list( name = "features", path = "features", dimension = "feature", layout = "data_frame", format = "parquet", mediatype = "application/vnd.apache.parquet", schema = list(fields = list(list(name = "id", type = "integer"))))) writeDatapackage("summarized_experiment", resources, tf) cat(readLines(file.path(tf, "datapackage.json")), sep = "\n")
An arrow::write_dataset wrapper function to write the Parquet
representation of various R objects using the Frictionless
[Data Package](https://datapackage.org) metadata model. This function
supports multiple object types including arrays, data frames, genomic
ranges, and more, converting them into efficient Parquet format for
analysis and storage with comprehensive metadata descriptions.
writeParquet(x, path, ...) ## S4 method for signature 'ANY' writeParquet( x, path, indexcols = names(dimnames(x)) %||% sprintf("index%d", seq_along(dim(x))), indexrefs = NULL, datacol = "value", grid = defaultAutoGrid(COO_SparseArray(dim(x))), grid_suffix = "_group", BPPARAM = getAutoBPPARAM(), name = basename(path), dimension = c("crossed", "unbound"), layout = "coord_array", ... ) ## S4 method for signature 'data.frame' writeParquet( x, path, indexcol = "__index__", keycol = "__name__", dimtbl = NULL, refs = NULL, append = FALSE, offset = 0L, part = NULL, part_digits = 0L, name = basename(path), dimension = c("unbound", "sample", "feature", "crossed"), layout = "data_frame", ... ) ## S4 method for signature 'DataFrame' writeParquet( x, path, indexcol = "__index__", keycol = "__name__", dimtbl = NULL, refs = NULL, append = FALSE, offset = 0L, part = NULL, part_digits = 0L, name = basename(path), dimension = c("unbound", "sample", "feature", "crossed"), layout = "data_frame", ... ) ## S4 method for signature 'DuckDBTable' writeParquet( x, path, indexcol = "__index__", keycol = "__name__", dimtbl = NULL, append = FALSE, offset = 0L, part = NULL, part_digits = 0L, name = basename(path), dimension = c("unbound", "sample", "feature", "crossed"), layout = "data_frame", ... ) ## S4 method for signature 'DuckDBDataFrame' writeParquet( x, path, indexcol = "__index__", keycol = "__name__", dimtbl = NULL, append = FALSE, offset = 0L, part = NULL, part_digits = 0L, name = basename(path), dimension = c("unbound", "sample", "feature", "crossed"), layout = "data_frame", ... ) ## S4 method for signature 'DuckDBTransposedDataFrame' writeParquet( x, path, indexcol = "__index__", keycol = "__name__", dimtbl = NULL, append = FALSE, offset = 0L, part = NULL, part_digits = 0L, name = basename(path), dimension = "crossed", layout = "transposed_data_frame", ... ) ## S4 method for signature 'DuckDBSelfHits' writeParquet( x, path, indexcol = "__index__", keycol = "__name__", dimtbl = NULL, append = FALSE, offset = 0L, part = NULL, part_digits = 0L, name = basename(path), dimension = c("unbound", "sample", "feature"), layout = "graph_edges", ... ) ## S4 method for signature 'DuckDBGRanges' writeParquet( x, path, indexcol = "__index__", keycol = "__name__", dimtbl = NULL, append = FALSE, offset = 0L, part = NULL, part_digits = 0L, name = basename(path), dimension = c("feature", "unbound"), layout = "genomic_ranges", ... ) ## S4 method for signature 'DuckDBGRangesList' writeParquet( x, path, indexcol = "__index__", keycol = "__name__", dimtbl = NULL, append = FALSE, offset = 0L, part = NULL, part_digits = 0L, name = basename(path), dimension = c("feature", "unbound"), layout = "genomic_ranges_list", ... ) ## S4 method for signature 'TransposedDataFrame' writeParquet( x, path, indexcol = "__index__", keycol = "__name__", dimtbl = NULL, name = basename(path), dimension = "crossed", layout = "transposed_data_frame", ... ) ## S4 method for signature 'list' writeParquet( x, path, dimension = c("unbound", "sample", "feature", "crossed"), ... ) ## S4 method for signature 'List' writeParquet( x, path, dimension = c("unbound", "sample", "feature", "crossed"), ... ) ## S4 method for signature 'GenomicRanges' writeParquet( x, path, indexcol = "__index__", keycol = "__name__", dimtbl = NULL, name = basename(path), dimension = c("feature", "unbound"), layout = "genomic_ranges", ... ) ## S4 method for signature 'GenomicRangesList' writeParquet( x, path, indexcol = "__index__", keycol = "__name__", dimtbl = NULL, name = basename(path), dimension = c("feature", "unbound"), layout = "genomic_ranges_list", ... ) ## S4 method for signature 'SelfHits' writeParquet( x, path, indexcol = "__index__", keycol = "__name__", dimtbl = NULL, name = basename(path), dimension = c("unbound", "sample", "feature"), layout = "graph_edges", ... ) ## S4 method for signature 'Assays' writeParquet( x, path, indexcols = c("__feature__", "__sample__"), indexrefs = NULL, datacol = "value", grid = defaultAutoGrid(COO_SparseArray(dim(x[[1L]]))), grid_suffix = "group__", dimension = "crossed", ... ) ## S4 method for signature 'SummarizedExperiment' writeParquet( x, path, indexcols = c("__feature__", "__sample__"), package = list(model = ifelse(.hasRealRowRanges(x), "ranged_summarized_experiment", "summarized_experiment"), resources = list()), ... ) ## S4 method for signature 'SingleCellExperiment' writeParquet( x, path, indexcols = c("__feature__", "__sample__"), package = list(model = "single_cell_experiment", resources = list()), ... ) ## S4 method for signature 'ExperimentList' writeParquet(x, path, indexcols = c("__feature__", "__sample__"), ...) ## S4 method for signature 'MultiAssayExperiment' writeParquet( x, path, indexcols = c("__feature__", "__sample__"), package = list(model = "multi_assay_experiment", resources = list()), ... ) ## S4 method for signature 'PointsLayerList' writeParquet(x, path, ...) ## S4 method for signature 'ShapesLayerList' writeParquet(x, path, ...) ## S4 method for signature 'MultiAssaySpatialExperiment' writeParquet( x, path, indexcols = c("__feature__", "__sample__"), package = list(model = "multi_assay_spatial_experiment", resources = list()), ... )writeParquet(x, path, ...) ## S4 method for signature 'ANY' writeParquet( x, path, indexcols = names(dimnames(x)) %||% sprintf("index%d", seq_along(dim(x))), indexrefs = NULL, datacol = "value", grid = defaultAutoGrid(COO_SparseArray(dim(x))), grid_suffix = "_group", BPPARAM = getAutoBPPARAM(), name = basename(path), dimension = c("crossed", "unbound"), layout = "coord_array", ... ) ## S4 method for signature 'data.frame' writeParquet( x, path, indexcol = "__index__", keycol = "__name__", dimtbl = NULL, refs = NULL, append = FALSE, offset = 0L, part = NULL, part_digits = 0L, name = basename(path), dimension = c("unbound", "sample", "feature", "crossed"), layout = "data_frame", ... ) ## S4 method for signature 'DataFrame' writeParquet( x, path, indexcol = "__index__", keycol = "__name__", dimtbl = NULL, refs = NULL, append = FALSE, offset = 0L, part = NULL, part_digits = 0L, name = basename(path), dimension = c("unbound", "sample", "feature", "crossed"), layout = "data_frame", ... ) ## S4 method for signature 'DuckDBTable' writeParquet( x, path, indexcol = "__index__", keycol = "__name__", dimtbl = NULL, append = FALSE, offset = 0L, part = NULL, part_digits = 0L, name = basename(path), dimension = c("unbound", "sample", "feature", "crossed"), layout = "data_frame", ... ) ## S4 method for signature 'DuckDBDataFrame' writeParquet( x, path, indexcol = "__index__", keycol = "__name__", dimtbl = NULL, append = FALSE, offset = 0L, part = NULL, part_digits = 0L, name = basename(path), dimension = c("unbound", "sample", "feature", "crossed"), layout = "data_frame", ... ) ## S4 method for signature 'DuckDBTransposedDataFrame' writeParquet( x, path, indexcol = "__index__", keycol = "__name__", dimtbl = NULL, append = FALSE, offset = 0L, part = NULL, part_digits = 0L, name = basename(path), dimension = "crossed", layout = "transposed_data_frame", ... ) ## S4 method for signature 'DuckDBSelfHits' writeParquet( x, path, indexcol = "__index__", keycol = "__name__", dimtbl = NULL, append = FALSE, offset = 0L, part = NULL, part_digits = 0L, name = basename(path), dimension = c("unbound", "sample", "feature"), layout = "graph_edges", ... ) ## S4 method for signature 'DuckDBGRanges' writeParquet( x, path, indexcol = "__index__", keycol = "__name__", dimtbl = NULL, append = FALSE, offset = 0L, part = NULL, part_digits = 0L, name = basename(path), dimension = c("feature", "unbound"), layout = "genomic_ranges", ... ) ## S4 method for signature 'DuckDBGRangesList' writeParquet( x, path, indexcol = "__index__", keycol = "__name__", dimtbl = NULL, append = FALSE, offset = 0L, part = NULL, part_digits = 0L, name = basename(path), dimension = c("feature", "unbound"), layout = "genomic_ranges_list", ... ) ## S4 method for signature 'TransposedDataFrame' writeParquet( x, path, indexcol = "__index__", keycol = "__name__", dimtbl = NULL, name = basename(path), dimension = "crossed", layout = "transposed_data_frame", ... ) ## S4 method for signature 'list' writeParquet( x, path, dimension = c("unbound", "sample", "feature", "crossed"), ... ) ## S4 method for signature 'List' writeParquet( x, path, dimension = c("unbound", "sample", "feature", "crossed"), ... ) ## S4 method for signature 'GenomicRanges' writeParquet( x, path, indexcol = "__index__", keycol = "__name__", dimtbl = NULL, name = basename(path), dimension = c("feature", "unbound"), layout = "genomic_ranges", ... ) ## S4 method for signature 'GenomicRangesList' writeParquet( x, path, indexcol = "__index__", keycol = "__name__", dimtbl = NULL, name = basename(path), dimension = c("feature", "unbound"), layout = "genomic_ranges_list", ... ) ## S4 method for signature 'SelfHits' writeParquet( x, path, indexcol = "__index__", keycol = "__name__", dimtbl = NULL, name = basename(path), dimension = c("unbound", "sample", "feature"), layout = "graph_edges", ... ) ## S4 method for signature 'Assays' writeParquet( x, path, indexcols = c("__feature__", "__sample__"), indexrefs = NULL, datacol = "value", grid = defaultAutoGrid(COO_SparseArray(dim(x[[1L]]))), grid_suffix = "group__", dimension = "crossed", ... ) ## S4 method for signature 'SummarizedExperiment' writeParquet( x, path, indexcols = c("__feature__", "__sample__"), package = list(model = ifelse(.hasRealRowRanges(x), "ranged_summarized_experiment", "summarized_experiment"), resources = list()), ... ) ## S4 method for signature 'SingleCellExperiment' writeParquet( x, path, indexcols = c("__feature__", "__sample__"), package = list(model = "single_cell_experiment", resources = list()), ... ) ## S4 method for signature 'ExperimentList' writeParquet(x, path, indexcols = c("__feature__", "__sample__"), ...) ## S4 method for signature 'MultiAssayExperiment' writeParquet( x, path, indexcols = c("__feature__", "__sample__"), package = list(model = "multi_assay_experiment", resources = list()), ... ) ## S4 method for signature 'PointsLayerList' writeParquet(x, path, ...) ## S4 method for signature 'ShapesLayerList' writeParquet(x, path, ...) ## S4 method for signature 'MultiAssaySpatialExperiment' writeParquet( x, path, indexcols = c("__feature__", "__sample__"), package = list(model = "multi_assay_spatial_experiment", resources = list()), ... )
x |
The object to write. Supported types include:
|
path |
A character string specifying the path where the data will be written. The path will be created if it doesn't exist. |
... |
Additional arguments. For
Note: |
indexcols |
For array-like objects, a character vector of column names
for the integer index columns in the resulting table. Defaults to the
|
indexrefs |
For array-like objects, an optional list of foreign key
references for the index columns in the schema. Defaults to |
datacol |
For array-like objects, a character string specifying the column name containing the array values in the resulting table. Defaults to "value". |
grid |
For array-like objects, an optional ArrayGrid
to use for partitioning array-like objects. If provided, the array will be
split into chunks and written as separate files. Defaults to
|
grid_suffix |
For array-like objects, a character string to append to the partitioning directories if the grid contains more than one cell. Defaults to "_group". |
BPPARAM |
For array-like objects, a
BiocParallelParam object to use for parallel processing
when |
name |
A character string specifying the name of the resource. Defaults
to the basename of the |
dimension |
A character string describing which biological axis the
resource is indexed by. One of |
layout |
A character string identifying the physical storage layout of
the resource. Recorded in |
indexcol |
For |
keycol |
For |
dimtbl |
For |
refs |
For flat ( |
append |
Logical. For |
offset |
Non-negative integer. For |
part |
Optional non-negative integer; |
part_digits |
Zero-padding width for |
package |
For experiment-level methods (
Callers should not normally set this; the default is supplied by each
experiment method. Experiment methods accumulate |
This function provides specialized handling for different object types:
Array-like objects: Converts multi-dimensional arrays into a
coordinate (long) format where each non-zero element is represented as a
row with columns for each dimension and the value. For sparse arrays, only
non-zero elements are written, making it efficient for sparse data. When a
grid is provided with multiple cells, the array is partitioned and each
partition is written to a separate subdirectory. Array writes delegate to
writeCoordArray; arguments
append, along, offset, group_offset,
arrowtype, and max_dim are forwarded via ... and
documented on the writeCoordArray help page.
Streaming a hive-partitioned coordinate array from a source with no
natural in-memory array representation (a database cursor, an API page,
etc.): there is no dedicated streaming writer for this case, but
append = TRUE already supports it. Accumulate each batch into a
small in-memory array slab (e.g. a Matrix::sparseMatrix or
SparseArray::COO_SparseArray) covering one or more NEW partition
groups, and call writeParquet(slab, path, grid = <grid for this
slab>, append = TRUE, along = <the growing dimension>, offset =
<cumulative extent written so far>, group_offset = <cumulative partition
groups written so far>) once per slab. Offsets and group offsets are
running counters with no requirement that slabs be a regular size, so an
irregular (calibration-based) flush schedule is fine. One real restriction
to plan around: append = TRUE requires length(grid) > 1L for
the slab's own grid (see ?writeCoordArray), so a slab that only
grows a single partition group in the streamed dimension must still split
some other, non-growing dimension into 2 grid cells purely to
satisfy that check. BiocDuckDB's own writers always pass
existing_data_behavior = "error" through to
write_dataset (never "delete_matching"), and
writeCoordArray's pre-write check refuses to write into a partition
directory that already exists – the silent-data-loss failure mode of
arrow::write_dataset(existing_data_behavior = "delete_matching")
(which deletes every existing file in any partition directory a call
touches, not just same-named ones) only applies to code that bypasses
writeCoordArray/writeParquet and calls
arrow::write_dataset directly with that flag.
Sizing each slab's grid (and, for a non-streamed array write, the
top-level grid argument) for a large or sparse array is not
automatic: the default, defaultAutoGrid(COO_SparseArray(dim(x))),
budgets partitions from dim(x) alone, as if x were fully
dense. See writeCoordArray's "Sizing
grid for large or sparse arrays" section for the density
correction (setAutoBlockSize() / setAutoBlockShape()) and
the hard ceiling on how far it can be pushed.
Flat table append (data.frame / DataFrame): Use
part, part_digits, append, and offset to stream
chunked sample or feature tables as part-0.parquet, part-1.parquet,
... without temporary directories. The global index column (when
indexcol is set) uses offset + seq_len(nrow(x)) on each chunk.
The first call returns a Frictionless resource list (one element); later
append parts return NULL. Build datapackage.json from the first
return, for example:
res <- writeParquet(chunk1, samples_dir, indexcol = "__sample__",
part = 0L, name = "samples", dimension = "sample")
pkg <- list(resources = res)
writeParquet(chunk2, samples_dir, indexcol = "__sample__",
offset = nrow(chunk1), part = 1L, append = TRUE, ...)
data.frame objects: Writes data frames directly with optional
row names and dimension lookup tables for partitioning information.
DataFrame objects: Writes data frames with schema properties
that describe column types and semantics. Column metadata (mcols) is
not preserved; use object-level metadata() for annotations.
TransposedDataFrame objects: Writes transposed data frames
with optional row names and dimension lookup tables for partitioning
information.
GenomicRanges objects: Writes genomic coordinates with proper
column naming and schema properties (genomicCoords) that describe the mapping
of genomic coordinate columns. Range metadata columns are written as regular
columns in the output.
GenomicRangesList objects: Separates list element metadata
from ranges data, writing them to different paths with schema properties
that describe how to split the ranges into list elements.
SelfHits objects: Writes graph edge lists as a data frame
with from, to, and optional metadata columns. The node count
(nnode) is stored in the schema properties to enable reconstruction as
a DuckDBSelfHits object.
Automatic unnesting of nested DataFrames: When writing
SingleCellExperiment objects, any DataFrame-class columns found in
rowData() or colData() are automatically extracted and moved to
rowTables() or colTables() respectively. This unnesting enables
independent Parquet serialization of multi-valued properties (e.g., multiple
diseases per patient, multiple isoforms per gene) while keeping the main
metadata tables flat and SQL-queryable. The original nested columns are
removed from rowData()/colData() after extraction. This
behavior is automatic and requires no user intervention.
Assays objects: Writes each assay in x to its own
assay_<name>/ subdirectory of path; the Frictionless
resource name recorded in datapackage.json is the bare
assay name (e.g. "counts") – only the on-disk directory carries
the assay_ prefix, so a resource's name and its path
are not the same string. Before delegating to the array writer, each
assay matrix is transposed and indexcols (together with
indexrefs and the partitioning grid) are reversed to
match: a feature sample matrix, written with
indexcols = c("__feature__", "__sample__"), is stored as
sample feature, with indexcols effectively
c("__sample__", "__feature__") for that call. The practical
consequence is the on-disk partition nesting order: a hive-partitioned
assay is laid out __sample__group__=.../__feature__group__=...
(sample outer, feature inner), not the feature-major order its own
indexcols argument would suggest in isolation. readParquet
transposes back on read, so the round trip through
writeParquet/readParquet hides all of this; it only
matters to code that partitions or writes a coordinate array directly
without going through writeParquet on an in-memory Assays
object – see the "Advanced: writing and attaching resources directly"
vignette section for that path.
SummarizedExperiment objects: Writes multi-assay experiments
with separate paths for feature data, sample data, and assay data.
RangedSummarizedExperiment objects: Extends
SummarizedExperiment functionality with genomic ranges for
features. rowRanges(x) is written only when at least one feature
has a non-empty range; otherwise rowData(x) is used, since
is(x, "RangedSummarizedExperiment") alone is TRUE for every
SingleCellExperiment even with no ranges ever provided.
SingleCellExperiment objects: Extends
SummarizedExperiment with single-cell specific data. All collections
are written as flat resource directories directly under path. The
directory name encodes both axis and type: feature_embeddings/,
sample_embeddings/, feature_table_<name>/,
sample_table_<name>/, feature_graph_<name>/,
sample_graph_<name>/, experiment_<name>/. The path prefix is
derived automatically from layout. Alternative experiments are written
directly to root (flattened, same pattern as MultiAssayExperiment
experiments). See the automatic unnesting section for details about
DataFrame columns in rowData()/colData().
MultiAssayExperiment objects: Writes multi-experiment studies
with separate paths for sample data, sample mapping, and experiment data.
For primitive methods (ANY, data.frame, DataFrame,
TransposedDataFrame, GenomicRanges, GenomicRangesList,
SelfHits, Assays, list, List,
ExperimentList, PointsLayerList, ShapesLayerList):
invisibly returns a list of Frictionless resource entries suitable for
inclusion in a datapackage.json resources array.
For experiment methods (SummarizedExperiment,
SingleCellExperiment, MultiAssayExperiment,
MultiAssaySpatialExperiment): invisibly returns NULL; the
datapackage.json is written to path as a side effect.
For flat multi-part table writes (data.frame with append = TRUE
and part > 0): invisibly returns NULL.
This function writes a datapackage.json file (Frictionless Data
Package v2.0) alongside the Parquet files. The file contains two levels of
metadata:
Package-level fields (top of datapackage.json):
model - Overall schema identifier. Determines which
readParquet reader is used to reconstruct the container object
(e.g. "single_cell_experiment", "multi_assay_experiment").
When absent, readParquet returns a SimpleList of resources
with no imposed schema.
annotations - Non-relational elements from metadata(x)
(scalars, vectors, 1-D arrays, JSON-safe nested lists). Tabular /
relational items (DataFrame, matrix, 2-D arrays) are
written as unbound Parquet sidecars and referenced here via
parquet_ref stubs; mixed nested lists use a
nested_mapping wrapper.
Per-resource fields (each entry in the resources array):
name - Resource identifier (typically the object name)
path - Relative directory path to the Parquet dataset
dimension - Biological axis: "feature", "sample",
"crossed", or "unbound"
layout - Physical storage layout (BiocDuckDB extension);
together with dimension, uniquely determines the R accessor and
AnnData slot used during reconstruction
format - File format ("parquet")
mediatype - MIME type ("application/vnd.apache.parquet")
schema - Field definitions including types, primary key,
sort order, and foreign key references
See the "Supported Object Types" section of ?readParquet for the
full model table and dimension + layout dispatch
table.
Patrick Aboyoun
readParquet for reading Bioconductor objects from
parquet, including the "Supported Object Types" table of per-model
resource names/paths
writeStreamingResource for streaming a flat
(non-array) resource from a block-producing callback instead of an
in-memory table
writeCoordArray for coord-array layout,
hive partitioning, and array append (append, along,
offset, group_offset)
parquet-io for shared Parquet I/O:
flat append validation (setupFlatParquetWrite, checkAppendPart,
validateAppendOffset), SQL COPY TO helpers
(buildParquetCopySQL), and lazy table export
(writeDuckDBTableParquet)
createDimTables for creating dimension lookup tables
ArrayGrid for grid partitioning
BiocParallelParam for parallel processing options
# Write the Titanic dataset to a single Parquet file tf1 <- tempfile() writeParquet(Titanic, tf1) list.files(tf1, full.names = TRUE, recursive = TRUE) # Write the state.x77 matrix to multiple Parquet files using grid partitioning tf3 <- tempfile() state_grid <- RegularArrayGrid(dim(state.x77), c(10, 4)) writeParquet(state.x77, file.path(tf3, "state"), grid = state_grid) dimtbls <- createDimTables(state.x77, grid = state_grid) list.files(tf3, full.names = TRUE, recursive = TRUE) # For a large, sparse matrix left to its default grid (grid = NULL), size # partitions off the real (sparse) data volume instead of the dense # default: setAutoBlockSize()/setAutoBlockShape() must be called before # writeParquet(), since grid = NULL computes the grid internally from # whatever is currently set (see ?writeCoordArray, "Sizing grid for # large or sparse arrays"). sparse_mat <- Matrix::rsparsematrix(2000, 500, density = 0.01) density <- Matrix::nnzero(sparse_mat) / prod(dim(sparse_mat)) setAutoBlockSize(1e6 / density) setAutoBlockShape("scale") tf4 <- tempfile() writeParquet(sparse_mat, tf4)# Write the Titanic dataset to a single Parquet file tf1 <- tempfile() writeParquet(Titanic, tf1) list.files(tf1, full.names = TRUE, recursive = TRUE) # Write the state.x77 matrix to multiple Parquet files using grid partitioning tf3 <- tempfile() state_grid <- RegularArrayGrid(dim(state.x77), c(10, 4)) writeParquet(state.x77, file.path(tf3, "state"), grid = state_grid) dimtbls <- createDimTables(state.x77, grid = state_grid) list.files(tf3, full.names = TRUE, recursive = TRUE) # For a large, sparse matrix left to its default grid (grid = NULL), size # partitions off the real (sparse) data volume instead of the dense # default: setAutoBlockSize()/setAutoBlockShape() must be called before # writeParquet(), since grid = NULL computes the grid internally from # whatever is currently set (see ?writeCoordArray, "Sizing grid for # large or sparse arrays"). sparse_mat <- Matrix::rsparsematrix(2000, 500, density = 0.01) density <- Matrix::nnzero(sparse_mat) / prod(dim(sparse_mat)) setAutoBlockSize(1e6 / density) setAutoBlockShape("scale") tf4 <- tempfile() writeParquet(sparse_mat, tf4)
Write a table that does not fit in memory to one flat, multi-part Parquet
resource by pulling it block-by-block from a producer callback. This owns the
streaming bookkeeping that every large producer would otherwise hand-roll on
top of writeParquet: the running row offset, the
part index and zero-padded part_digits, the append flag
(part 0 creates the directory, later parts append), the positional
dimtbl slice per block, the index_max threading that keeps the
__index__ column one consistent (possibly >2^31) integer type across
parts, and the post-write integrity checks. The single descriptor it returns
is ready to hand to writeDatapackage.
writeStreamingResource( blocks, path, dimension, indexcol = "__index__", keycol = "__name__", layout = "data_frame", refs = NULL, dimtbl = NULL, cluster_by = NULL, index_max = NULL, expected_rows = NULL, part_digits = 6L, name = basename(path), ... )writeStreamingResource( blocks, path, dimension, indexcol = "__index__", keycol = "__name__", layout = "data_frame", refs = NULL, dimtbl = NULL, cluster_by = NULL, index_max = NULL, expected_rows = NULL, part_digits = 6L, name = basename(path), ... )
blocks |
A function of one argument, the 1-based part index |
path |
Directory for the resource's Parquet parts. |
dimension |
Biological axis of the resource: one of |
indexcol, keycol
|
Passed to |
layout |
Physical layout string (default |
refs, cluster_by
|
Passed to |
dimtbl |
An optional full, index-ordered dimension-table
|
index_max |
Optional upper bound on the row index (the resource's total
row count, or |
expected_rows |
Optional expected total row count. When given and the streamed total differs, a warning is emitted (an unbound junction table can legitimately differ; a bound one usually indicates an off-by-one or a dropped block). |
part_digits |
Zero-padding width for the part index in filenames. The
reader globs |
name |
Resource name recorded in the descriptor (default
|
... |
Further arguments forwarded to |
Index typing / the narrowing floor. writeParquet types
part 0's __index__ column by the index range it sees. Without
index_max, part 0 is typed to its own [0, nrow] range, so a
small part 0 can pick a narrow integer type (e.g. uint16) that a later
part's larger index overflows on append. Passing index_max (the total
row count, or Inf) types the index for the whole resource up front and
removes this hazard entirely. When index_max is not given and part 0
has fewer than 65536 rows in a multi-part stream, a warning is emitted.
Partition alignment. The per-block dimtbl slice assumes the
stream is dense and contiguous with exactly nrow(dimtbl) rows in index
order. If the streamed total does not equal nrow(dimtbl) the partition
column would be misaligned (silently corrupting directory pruning), so this
is a fatal error rather than a warning.
Commit boundary. A multi-part resource is complete only once every
part is written. To make "readable only when complete" hold by construction,
an _INCOMPLETE marker file is dropped into path immediately
before part 0 is written and removed once the stream finishes, so a crash
during part 0 is covered too. A finished resource is therefore a clean
directory of only parquet parts that the reader loads normally, while an
interrupted write leaves the marker behind; DuckDBDataFrame refuses
to open a directory carrying the marker, so a read of an incomplete
resource fails loudly instead of silently returning the partial parts. A
fatal partition-alignment error likewise leaves the marker in place.
Invisibly, the single Frictionless resource descriptor (a list) captured from
part 0, with an added n_rows count, ready for
writeDatapackage. NULL if the stream yielded no rows.
Patrick Aboyoun
writeParquet for the single-block writer;
writeDatapackage to assemble the resources into a
datapackage.json.
tf <- tempfile() on.exit(unlink(tf, recursive = TRUE)) chunks <- list(data.frame(v = 1:3), data.frame(v = 4:5)) res <- writeStreamingResource( function(i) if (i <= length(chunks)) chunks[[i]] else NULL, path = file.path(tf, "samples"), dimension = "unbound", keycol = NULL, index_max = 5, expected_rows = 5) res[[1]][["n_rows"]] # 5 list.files(file.path(tf, "samples")) # part-000000.parquet, part-000001.parquettf <- tempfile() on.exit(unlink(tf, recursive = TRUE)) chunks <- list(data.frame(v = 1:3), data.frame(v = 4:5)) res <- writeStreamingResource( function(i) if (i <= length(chunks)) chunks[[i]] else NULL, path = file.path(tf, "samples"), dimension = "unbound", keycol = NULL, index_max = 5, expected_rows = 5) res[[1]][["n_rows"]] # 5 list.files(file.path(tf, "samples")) # part-000000.parquet, part-000001.parquet