--- title: "Example datasets and reproducible workflows" output: rmarkdown::html_vignette: toc: true vignette: > %\VignetteIndexEntry{Example datasets and reproducible workflows} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>", fig.width = 6, fig.height = 4) library(linf) # Keep table rendering separate from the runnable analysis code. knitr::opts_chunk$set(render = function(x, options) { rendered <- knitr::knit_print(x, options = options) if (inherits(x, "knitr_kable")) { knitr::asis_output(paste0( '
\n\n', rendered, '\n\n
\n' )) } else rendered }) ``` Choose a matrix if you want to fit a hierarchy, an assignment table if you want to inspect a saved grouping, and the four-component Valencia subset if you want to explore coordinate charts. The package supplies five datasets and small examples can be constructed directly in R; it has no synthetic-data generation API. The [function guide](function-guide.html) explains how each stage fits together. ## Choose a dataset Tables scroll horizontally on narrow screens. Focus a table and use the arrow keys, or swipe within it, to read every column without breaking function or dataset names. Every object below is loaded with `data(object_name, package = "linf")` and has its own help page, for example `help("agp_gut", package = "linf")`.
| Dataset | Unit, orientation and dimensions | Identifiers and main fields | Suitable workflow | |---|---|---|---| | `agp_gut` | 766 gut samples by 314 taxa; integer read-count matrix. | `counts`, `meta`, `taxa`, `source`; row names join to `meta$Run` (SRA accessions). | [Count filtering and fitting](#reference-and-query). | | `valencia2k` | 2,000 vaginal samples by 178 taxa; unit-sum relative abundances. | `rel`, `cst`, `reads`, `taxa`, `source`; local row IDs join to `cst$sample_id`; `cst$source_row` joins to the pinned source. | [Align metadata](#align-data-and-metadata), then follow the [vaginal tutorial](linf-vaginal.html). | | `valencia13k_dcst_depth2_merged` | 12,881 source samples; assignment table with six columns, not an abundance matrix. | `assignments`, `summaries`, `feature_labels`, `params`, `source`; `sample_id` and `source_row`. | Inspect saved absorb-policy depth-1/2 assignments and sizes. | | `valencia13k_dcst_depth3_merged` | Same 12,881 source samples; assignment table with seven columns. | Same fields as depth 2, plus `dcst_depth3` and a depth-3 summary. | Inspect the saved deeper grouping and compare depths. | | `valencia_linf_hypercube_1k` | 1,000 vaginal samples by four selected taxa; unit-sum matrix. | `rel4`, `meta`, `component_map`, `source`; local `sample_id`, source-row index and selected mass. | [Compute a static chart](#a-static-chart-from-the-four-component-data); coordinates are not precomputed. |
The two merged objects are ordinary lists of assignments and summary tables. They lack the complete `linf.csts` object and input matrix required by `refine.linf.csts()`, `dcst.view()`, `linf.landmarks()` and `transfer.dcsts()`. Do not change their class to make them look like fresh fits. ```{r inventory} data(agp_gut, package = "linf") data(valencia2k, package = "linf") data(valencia13k_dcst_depth2_merged, package = "linf") data(valencia13k_dcst_depth3_merged, package = "linf") data(valencia_linf_hypercube_1k, package = "linf") observed <- rbind( gut_counts = dim(agp_gut$counts), vaginal_rel = dim(valencia2k$rel), merged_depth2 = dim(valencia13k_dcst_depth2_merged$assignments), merged_depth3 = dim(valencia13k_dcst_depth3_merged$assignments), four_component_rel = dim(valencia_linf_hypercube_1k$rel4) ) colnames(observed) <- c("Rows", "Columns") stopifnot(identical(unname(observed), matrix(c(766L, 2000L, 12881L, 12881L, 1000L, 314L, 178L, 6L, 7L, 4L), ncol = 2))) knitr::kable(observed, caption = "Dimensions read from the bundled objects") ``` ## Provenance and scope ### Gut counts `agp_gut` comes from American Gut Project public 16S V4 records under [PRJEB11419](https://www.ebi.ac.uk/ena/browser/view/PRJEB11419), processed to SILVA taxonomic counts through PRIME. The source preparation first selected up to 5,000 samples with at least 1,000 reads. The 766-sample bundle includes all samples assigned to four selected uncommon demonstration dCSTs (Prevotella_7, Pasteurellaceae, Akkermansia and Staphylococcus), excludes Eukaryota/Unassigned depth-1 labels, and fills remaining slots from the eligible background using Python's seeded random sampling with seed 42. The stored `dcst_depth1`/`dcst_depth2` metadata were derived in the source analysis; refitting on this selected subset need not reproduce them. Phenotype fields were joined **after** membership was fixed. Selection differs by dCST, so this is not a probability sample for population inference. Self-reported phenotypes are available for alignment demonstrations, not general prevalence, association, causal or clinical claims. They are not independent validation labels. Counts retain the limitations of 16S taxonomic assignment and the documented caution about Escherichia-Shigella in V4 data. ### Valencia abundance data and saved assignments The four Valencia-derived objects use the filtered training source for the [VALENCIA classifier](https://github.com/ravel-lab/VALENCIA) described by [France et al. (2020)](https://doi.org/10.1186/s40168-020-00934-6). The prepared source has 12,881 samples and 178 taxa. `valencia2k` was sampled with R seed 42 within the 13 supplied sub-CST groups, approximately preserving their proportions with rounding and a minimum of five per stratum. `valencia2k$reads` contains post-taxonomic-filter read totals. Multiplying `rel` by `reads` gives **count-like reconstructions**; rounding them does not recover or certify original observed integer counts. Use `rel` directly for normalization and rank-based fitting. The upstream filtering is described in the existing [vaginal tutorial](linf-vaginal.html); the subset-building script takes that prepared matrix as input and does not repeat its filtering. The supplied `Val_CST` and `Val_subCST` labels belong to the Valencia training source. A comparison made on these bundled rows overlaps that source, even though dCST fitting does not use its class labels. Such agreement is descriptive, not held-out validation of a classifier or evidence of clinical performance. The merged assignment assets were built on the full prepared source with `n0 = 50`, `low.freq.policy = "absorb"` and `refinement.factor = 2`, with one or two refinements. Their summaries contain `depth`, `dcst_label`, `n`, `prop` and `path_length`. Proportions refer to this source sample set. Path lengths may be shorter than the stored depth for terminal lineages; treat saved labels as labels, not as a source of stable feature IDs. ### Four-component data `valencia_linf_hypercube_1k` selects Lactobacillus iners (`Li`), Lactobacillus crispatus (`Lc`), Gardnerella vaginalis (`Gv`) and BVAB1 (`Bv`). Rows with zero total mass in those four taxa were removed, and retained rows were renormalized to sum to one **over those four features**. R seed 20261604 was used for a 1,000-row sample stratified by the dominant selected component. `meta$selected_mass` records how much of the original full profile these four taxa represented. A large fraction within `rel4` can still correspond to little of the full community. `component_map` connects abbreviations to source taxa; neither the subset nor its charts replace the full 178-feature profiles. All objects include a provenance string. The installed provenance file records source preparation and upstream terms (which are separate from the code's MIT license): ```{r provenance-location} provenance <- system.file("DATA_PROVENANCE.md", package = "linf", mustWork = TRUE) cat(readLines(provenance, warn = FALSE)[1:12], sep = "\n") ``` Read the full installed record at the console with `cat(readLines(provenance), sep = "\n")`. This works offline. Repository readers can open [data provenance](https://github.com/pgajer/linf/blob/main/inst/DATA_PROVENANCE.md), the [input manifest](https://github.com/pgajer/linf/blob/main/inst/DATA_MANIFEST.csv), and the [reproduction recipe](https://github.com/pgajer/linf/blob/main/data-raw/README.md). ## Align data and metadata Sample names are keys within an object, not universal cross-dataset IDs. For `valencia2k`, IDs such as `s1` were reassigned after sampling. Its verified `cst$source_row` now preserves the sampled row in the pinned prepared source. The four-component and merged objects share that `source_row` numbering. Join on this explicit key after checking provenance, never by row position or an apparent numeric suffix in the different local sample-ID schemes. ```{r alignment} X <- agp_gut$counts stopifnot(!anyDuplicated(agp_gut$meta$Run), !anyDuplicated(agp_gut$taxa)) sample.order <- match(rownames(X), agp_gut$meta$Run) feature.order <- match(colnames(X), agp_gut$taxa) stopifnot(!anyNA(sample.order), !anyNA(feature.order)) meta <- agp_gut$meta[sample.order, , drop = FALSE] features <- data.frame( id = paste0("asv_", seq_len(ncol(X))), taxonomy = agp_gut$taxa[feature.order] ) stopifnot(identical(rownames(X), meta$Run)) # After filtering, indices refer to the supplied X, in its original order. filtered <- filter.asv(X, min.lib = 1000, prev.prop = 0.05, min.count = 2, verbose = FALSE) meta.filtered <- meta[filtered$kept.sample.idx, , drop = FALSE] features.filtered <- features[filtered$kept.feature.idx, , drop = FALSE] stopifnot(identical(rownames(filtered$counts), meta.filtered$Run), nrow(features.filtered) == ncol(filtered$counts)) ``` The stable IDs here describe the original column order and are subsetted with the features; they are not regenerated after filtering. Pass `features.filtered$id` and display labels made from its taxonomy in that same order when fitting. Keep the mapping with the fit. Landmarks preserve complete stable IDs, including taxonomy strings containing `__`; see [landmark interpretation](function-guide.html#landmarks-are-selected-observations). The following join uses the explicit shared source-row key: ```{r shared-source-join} merged <- valencia13k_dcst_depth3_merged chart.meta <- valencia_linf_hypercube_1k$meta stopifnot(!anyDuplicated(merged$assignments$source_row)) join <- match(chart.meta$source_row, merged$assignments$source_row) stopifnot(!anyNA(join)) chart.depth3 <- merged$assignments$dcst_depth3[join] stopifnot(identical(as.character(chart.meta$Val_CST), as.character(merged$assignments$Val_CST[join]))) length(chart.depth3) subset.join <- match(valencia2k$cst$source_row, merged$assignments$source_row) stopifnot(!anyNA(subset.join), identical(as.character(valencia2k$cst$Val_CST), as.character(merged$assignments$Val_CST[subset.join]))) ``` This attaches in-source saved labels; it is not a new reference/query experiment. ## Constructed edge cases This deterministic matrix needs no random seed. It includes two supported dominant features, a low-support feature, a positive tie, an exact-zero row and a small row left unchanged by a positive tolerance. IDs differ from labels. ```{r toy-edge-cases} toy <- rbind( a1 = c(10, 3, 1), a2 = c(9, 2, 1), b1 = c(2, 10, 1), b2 = c(3, 9, 1), low_support = c(1, 2, 10), tie = c(4, 4, 0), zero = c(0, 0, 0), small = c(0.01, 0.005, 0) ) colnames(toy) <- c("asv_1", "asv_2", "asv_3") display <- c("Taxon A", "Taxon B", "Taxon C") M <- normalize.linf(toy, tol = 0.02) d <- linf.dominant.features(M, feature.labels = display) fit <- linf.csts(M, feature.labels = display, n0 = 2) edge.table <- data.frame( sample = rownames(M), maximum = apply(M, 1, max), dominant_ID = d$id, pure = fit$lineage.label.pure, absorb = fit$lineage.label.absorb ) knitr::kable(edge.table, row.names = FALSE) stopifnot(is.na(d$id[7]), d$id[6] == "asv_1", d$id[8] == "asv_1", identical(unname(M["small", ]), unname(toy["small", ]))) ``` The tied row chooses the first column. The small row retains maximum 0.01 and still has a dominant feature. The zero row has no dominant feature; pure fitting puts it in the rare bucket and absorb fitting uses the most-supported retained state. The low-support row is absorbed into Taxon B, its largest retained feature. Neither absorption is evidence that the assigned feature was the original maximum. For random ties, reset the seed before each call you want to reproduce: ```{r random-tie} set.seed(17) random1 <- linf.dominant.features(M, tie.method = "random")$id set.seed(17) random2 <- linf.dominant.features(M, tie.method = "random")$id stopifnot(identical(random1, random2)) ``` ## Reference and query Use a small seeded sample of 160 Valencia rows. The first 120 sampled rows form the reference; the other 40 are kept out of fitting and refinement. This split demonstrates frozen transfer mechanics within one source dataset, not external validation, and no outcome or CST label is used to fit the reference. ```{r real-transfer} set.seed(20260914) rows <- sample(seq_len(nrow(valencia2k$rel)), 160) reference.rows <- rows[1:120] query.rows <- rows[121:160] stopifnot(length(intersect(reference.rows, query.rows)) == 0L) reference <- valencia2k$rel[reference.rows, , drop = FALSE] query <- valencia2k$rel[query.rows, , drop = FALSE] reference.M <- normalize.linf(reference) reference.fit <- linf.csts(reference.M, n0 = 5, low.freq.policy = "absorb") reference.fit <- refine.linf.csts( reference.M, reference.fit, n0 = 3, refinement.factor = 2, low.freq.policy = "absorb", verbose = FALSE ) # Deliberately reverse query columns. Matching uses the unchanged feature IDs. query <- query[, rev(seq_len(ncol(query))), drop = FALSE] stopifnot(setequal(colnames(query), reference.fit$feature.ids)) transferred <- transfer.dcsts(query, reference.fit, match.by = "feature.ids") knitr::kable(data.frame( depth = colnames(transferred$assignment), assigned = colSums(!is.na(transferred$assignment)), unassigned = colSums(is.na(transferred$assignment)) ), row.names = FALSE) ``` No query filtering rule or hierarchy is learned here. Missing reference columns would be filled with zero; extra query columns would be ignored. Counts and positive row-scaled abundances give the same ranking for this transfer. `NA` means the walk found no positive realized candidate at that depth; it does not create a new dCST. A partial depth-1 assignment can coexist with a depth-2 `NA`. ```{r no-matching-feature} unmatched <- matrix(5, nrow = 1, ncol = 1, dimnames = list("unmatched_query", "not_in_reference")) no.assignment <- transfer.dcsts(unmatched, reference.fit)$assignment stopifnot(all(is.na(no.assignment))) no.assignment ``` For a longer count-based workflow use the [gut demonstration](linf-intro.html); for descriptive comparisons to source CST labels use the [vaginal workflow](linf-vaginal.html). ## A static chart from the four-component data The returned chart has three coordinates. This plot shows two of them for all 1,000 bundled samples; color identifies which of the four selected components has the largest abundance. It is a projection of a representation, with no learned classifier or biological-separation test. ```{r static-chart, fig.cap="Two coordinates of the Li-reference chart. Colors describe dominance within the selected four taxa; overlapping points and the omitted third coordinate limit what this projection shows."} four <- valencia_linf_hypercube_1k embedding <- linf.hypercube.embedding(four$rel4, reference = "Li") stopifnot(identical(dim(embedding), c(1000L, 3L)), all(is.finite(embedding))) colors <- c(Li = "#0072B2", Lc = "#D55E00", Gv = "#009E73", Bv = "#CC79A7") plot(embedding[, 1], embedding[, 2], col = colors[as.character(four$meta$dominant_component)], pch = 16, cex = 0.5, xlim = c(0, 1), ylim = c(0, 1), xlab = "Lc coordinate (reference Li)", ylab = "Gv coordinate (reference Li)") legend("topright", legend = names(colors), col = colors, pch = 16, title = "Dominant component", bty = "n", cex = 0.8) ``` Check `summary(four$meta$selected_mass)` before interpreting the four-taxon composition. The optional [interactive embedding article](https://pgajer.github.io/linf/articles/valencia-hypercube-embedding.html) adds three-dimensional views and is website-only. The four installed vignettes are listed by `browseVignettes("linf")`; their normal builds need no network downloads, interactive device or external dataset.