Community state types (CSTs) are the standard way to describe the structure of vaginal microbial communities. They were originally defined by hierarchical clustering of 16S rRNA data (Ravel et al. 2011) and later standardized by the VALENCIA nearest-centroid classifier (France et al. 2020).
Dominant Community State Types (dCSTs) group samples by their largest features and retain groups meeting a sample-support threshold. Fitting uses abundances rather than supplied CST labels; the retained states still depend on the fitting sample set. L-infinity normalization divides positive rows by their maximum at default tolerance, preserving their within-row rankings.
This vignette compares dCSTs with supplied Valencia labels on source training samples. It is a descriptive comparison, not held-out validation. See the function guide for fitting versus frozen transfer and the dataset guide for provenance and alignment.
Reference: Gajer, P. & Ravel, J. (2025). A New Approach to Compositional Data Analysis using \(L^\infty\)-normalization with Applications to Vaginal Microbiome. arXiv:2503.21543.
The valencia2k dataset is a stratified subsample of
2,000 vaginal samples from the Valencia CST-classifier training set
(France et al. 2020). It contains species-level relative abundances for
178 taxa along with the original Valencia CST and sub-CST
assignments.
data(valencia2k)
## Compositional matrix: 2000 samples x 178 taxa
dim(valencia2k$rel)
#> [1] 2000 178
## Valencia CST assignments
table(valencia2k$cst$Val_CST)
#>
#> I II III IV-A IV-B IV-C V
#> 557 69 582 141 454 117 80
## Top 10 most abundant taxa (by mean relative abundance)
means <- sort(colMeans(valencia2k$rel), decreasing = TRUE)
head(round(means, 4), 10)
#> Lactobacillus_iners Lactobacillus_crispatus Gardnerella_vaginalis
#> 0.2609 0.2539 0.1203
#> Lactobacillus_jensenii Atopobium_vaginae BVAB1
#> 0.0418 0.0370 0.0297
#> Lactobacillus_gasseri Sneathia_sanguinegens g_Streptococcus
#> 0.0283 0.0198 0.0194
#> g_Megasphaera
#> 0.0158The dominant taxa are familiar vaginal species: Lactobacillus iners, L. crispatus, Gardnerella vaginalis, and others. This is exactly the kind of data where dominant-feature assignments are most intuitive: most samples are dominated by a single species, and the identity of that species is biologically meaningful.
The bundled matrix is compositional (rows sum to 1). For workflows
that require count-like input (e.g., filter.asv()), we can
reconstruct approximate counts using the per-sample read depths. These
reconstructed and rounded values are not original observed counts:
count_mat <- sweep(valencia2k$rel, 1, valencia2k$reads, "*")
count_mat <- round(count_mat)
storage.mode(count_mat) <- "integer"
## Check: library sizes
summary(rowSums(count_mat))
#> Min. 1st Qu. Median Mean 3rd Qu. Max.
#> 3026 22281 47958 57111 74849 409411The bundled Valencia matrix was already filtered upstream. The
subset-building script takes that prepared matrix as input; we skip
filter.asv() here and work directly with the compositional
matrix.
\(L^\infty\)-normalization divides
each row by its maximum value. After normalization at default tolerance,
every positive row has at least one coordinate equal to 1. Exact-zero
rows remain zero, and rows at or below a positive tol
remain unchanged. All rows in this example are positive.
The dominant species is the column that achieves the maximum in each row. Samples with the same dominant species form a depth-1 dominance sample set. With the default first-column tie rule, dominant-feature assignment is deterministic and sample-independent: it depends only on the sample’s own abundance profile, never on other samples.
dominant.features <- linf.dominant.features(Z)
## How many distinct dominant taxa?
cat("Distinct dominant taxa:", length(dominant.features$observed.levels), "\n")
#> Distinct dominant taxa: 35
## Dominance sample-set size distribution (top 15)
dominance_tab <- sort(table(dominant.features$label), decreasing = TRUE)
head(dominance_tab, 15)
#>
#> Lactobacillus_iners Lactobacillus_crispatus Gardnerella_vaginalis
#> 606 564 360
#> BVAB1 Lactobacillus_jensenii Lactobacillus_gasseri
#> 91 67 62
#> Atopobium_vaginae Sneathia_sanguinegens g_Streptococcus
#> 51 40 36
#> g_Bifidobacterium g_Anaerococcus g_Enterococcus
#> 26 14 13
#> g_Corynebacterium_1 BVAB_TM7 g_Staphylococcus
#> 11 8 7We see the expected vaginal community structure: a few well-supported dominance sample sets (the Lactobacillus species, Gardnerella, BVAB1) and a long tail of rarer dominant taxa.
In practice, many provisional dominance sample sets contain very few samples. Dominant Community State Types (dCSTs) retain states at or above a minimum support threshold (\(n_0\)) and handle samples from low-support sets according to the selected policy.
The package supports two policies for handling low-support provisional sets:
RARE_DOMINANTdcst1 <- linf.csts(Z, n0 = 30, low.freq.policy = "pure")
## Retained depth-1 states
dcst1$retained.feature.labels
#> [1] "Lactobacillus_iners" "Lactobacillus_crispatus"
#> [3] "Gardnerella_vaginalis" "BVAB1"
#> [5] "Lactobacillus_jensenii" "Lactobacillus_gasseri"
#> [7] "Atopobium_vaginae" "Sneathia_sanguinegens"
#> [9] "g_Streptococcus"
## dCST assignment (pure policy)
dcst1_tab <- sort(table(dcst1$lineage.label), decreasing = TRUE)
dcst1_tab
#>
#> Lactobacillus_iners Lactobacillus_crispatus Gardnerella_vaginalis
#> 606 564 360
#> RARE_DOMINANT BVAB1 Lactobacillus_jensenii
#> 123 91 67
#> Lactobacillus_gasseri Atopobium_vaginae Sneathia_sanguinegens
#> 62 51 40
#> g_Streptococcus
#> 36The following cross-tabulation describes all 2,000 bundled rows, which were subsampled from the Valencia classifier training source. The dCST hierarchy was fitted on those same rows, without using their supplied CST labels. This overlap precludes interpreting agreement as held-out validation:
## Build concordance table: rows = dCSTs, columns = Valencia CSTs
dcst_labels <- dcst1$lineage.label.absorb # use absorb view for clean comparison
metadata.order <- match(rownames(Z), valencia2k$cst$sample_id)
stopifnot(!anyNA(metadata.order), !anyDuplicated(valencia2k$cst$sample_id))
val_cst <- valencia2k$cst$Val_CST[metadata.order]
concordance <- table(dCST = dcst_labels, Valencia = val_cst)
## Show as percentage: what fraction of each dCST falls in each Valencia CST?
pct <- round(100 * prop.table(concordance, margin = 1), 1)
pct
#> Valencia
#> dCST I II III IV-A IV-B IV-C V
#> Atopobium_vaginae 0.0 0.0 0.0 4.9 88.5 3.3 3.3
#> BVAB1 0.0 0.0 0.0 100.0 0.0 0.0 0.0
#> g_Streptococcus 0.0 0.0 0.0 0.0 0.0 100.0 0.0
#> Gardnerella_vaginalis 0.0 1.3 0.3 8.2 86.9 3.3 0.0
#> Lactobacillus_crispatus 95.9 0.0 1.0 0.2 0.0 2.9 0.0
#> Lactobacillus_gasseri 0.0 98.4 0.0 0.0 0.0 1.6 0.0
#> Lactobacillus_iners 0.0 0.2 91.4 0.6 4.0 2.1 1.8
#> Lactobacillus_jensenii 0.0 0.0 0.0 0.0 0.0 4.3 95.7
#> Sneathia_sanguinegens 0.0 0.0 2.3 13.6 81.8 2.3 0.0Several dCSTs map predominantly to a single supplied Valencia CST in this source subset:
## Reverse view: what fraction of each Valencia CST falls in each dCST?
pct_rev <- round(100 * prop.table(concordance, margin = 2), 1)
pct_rev
#> Valencia
#> dCST I II III IV-A IV-B IV-C V
#> Atopobium_vaginae 0.0 0.0 0.0 2.1 11.9 1.7 2.5
#> BVAB1 0.0 0.0 0.0 67.4 0.0 0.0 0.0
#> g_Streptococcus 0.0 0.0 0.0 0.0 0.0 57.3 0.0
#> Gardnerella_vaginalis 0.0 7.2 0.2 22.7 74.7 11.1 0.0
#> Lactobacillus_crispatus 100.0 0.0 1.0 0.7 0.0 14.5 0.0
#> Lactobacillus_gasseri 0.0 91.3 0.0 0.0 0.0 0.9 0.0
#> Lactobacillus_iners 0.0 1.4 98.6 2.8 5.5 11.1 13.8
#> Lactobacillus_jensenii 0.0 0.0 0.0 0.0 0.0 2.6 83.8
#> Sneathia_sanguinegens 0.0 0.0 0.2 4.3 7.9 0.9 0.0The two tables describe overlap between different grouping rules on shared source samples. They do not estimate performance on an independent cohort.
Large dCSTs can be refined by dropping the dominant species and repeating the procedure on the remaining taxa. This produces a two-level hierarchy that reveals sub-community structure.
dcst2 <- refine.linf.csts(Z, dcst1, n0 = 15, refinement.factor = 2,
low.freq.policy = "pure", verbose = FALSE)
## Depth-2 dCST table
dcst2_tab <- sort(table(dcst2$lineage.label), decreasing = TRUE)
dcst2_tab
#>
#> Lactobacillus_iners__Gardnerella_vaginalis
#> 204
#> Lactobacillus_crispatus__Lactobacillus_jensenii
#> 151
#> Lactobacillus_crispatus__Lactobacillus_iners
#> 131
#> RARE_DOMINANT
#> 123
#> Lactobacillus_iners__RARE_DOMINANT
#> 105
#> Lactobacillus_iners__Lactobacillus_jensenii
#> 102
#> Lactobacillus_iners__Lactobacillus_crispatus
#> 101
#> Gardnerella_vaginalis__RARE_DOMINANT
#> 95
#> Gardnerella_vaginalis__Atopobium_vaginae
#> 94
#> Lactobacillus_crispatus__g_Lactobacillus
#> 88
#> Gardnerella_vaginalis__Lactobacillus_iners
#> 85
#> BVAB1__Gardnerella_vaginalis
#> 63
#> Lactobacillus_crispatus__RARE_DOMINANT
#> 63
#> Lactobacillus_gasseri
#> 62
#> Lactobacillus_crispatus__g_Corynebacterium_1
#> 42
#> Atopobium_vaginae__Gardnerella_vaginalis
#> 37
#> g_Streptococcus
#> 36
#> Gardnerella_vaginalis__Sneathia_sanguinegens
#> 33
#> Gardnerella_vaginalis__BVAB1
#> 29
#> BVAB1__RARE_DOMINANT
#> 28
#> Lactobacillus_iners__g_Corynebacterium_1
#> 27
#> Lactobacillus_iners__g_Streptococcus
#> 26
#> Gardnerella_vaginalis__g_Fastidiosipila
#> 24
#> Lactobacillus_jensenii__Lactobacillus_iners
#> 24
#> Lactobacillus_jensenii__RARE_DOMINANT
#> 24
#> Sneathia_sanguinegens__RARE_DOMINANT
#> 24
#> Lactobacillus_iners__g_Finegoldia
#> 22
#> Lactobacillus_crispatus__Gardnerella_vaginalis
#> 21
#> Lactobacillus_crispatus__Lactobacillus_reuteri
#> 21
#> Lactobacillus_iners__Lactobacillus_gasseri
#> 19
#> Lactobacillus_jensenii__Gardnerella_vaginalis
#> 19
#> Lactobacillus_crispatus__Lactobacillus_gasseri
#> 17
#> Sneathia_sanguinegens__Gardnerella_vaginalis
#> 16
#> Lactobacillus_crispatus__g_Clostridium_sensu_stricto_1
#> 15
#> Lactobacillus_crispatus__g_Finegoldia
#> 15
#> Atopobium_vaginae__RARE_DOMINANT
#> 14dcst2_labels <- dcst2$lineage.label.absorb
val_subcst <- valencia2k$cst$Val_subCST[metadata.order]
concordance2 <- table(dCST = dcst2_labels, Valencia = val_subcst)
## Percentage by dCST (row-wise)
pct2 <- round(100 * prop.table(concordance2, margin = 1), 1)
## Show only dCSTs with at least 20 samples for readability
dcst2_sizes <- rowSums(concordance2)
pct2[dcst2_sizes >= 20, , drop = FALSE]
#> Valencia
#> dCST I-A I-B II III-A III-B
#> Atopobium_vaginae__Gardnerella_vaginalis 0.0 0.0 0.0 0.0 0.0
#> BVAB1__Gardnerella_vaginalis 0.0 0.0 0.0 0.0 0.0
#> g_Streptococcus 0.0 0.0 0.0 0.0 0.0
#> Gardnerella_vaginalis 0.0 0.0 0.0 0.0 0.0
#> Gardnerella_vaginalis__Atopobium_vaginae 0.0 0.0 0.7 0.0 0.0
#> Gardnerella_vaginalis__BVAB1 0.0 0.0 0.0 0.0 0.0
#> Gardnerella_vaginalis__g_Fastidiosipila 0.0 0.0 0.0 0.0 0.0
#> Gardnerella_vaginalis__Lactobacillus_iners 0.0 0.0 3.3 0.0 0.8
#> Gardnerella_vaginalis__Sneathia_sanguinegens 0.0 0.0 0.0 0.0 0.0
#> Lactobacillus_crispatus__g_Corynebacterium_1 58.0 42.0 0.0 0.0 0.0
#> Lactobacillus_crispatus__g_Finegoldia 60.0 40.0 0.0 0.0 0.0
#> Lactobacillus_crispatus__g_Lactobacillus 90.2 9.8 0.0 0.0 0.0
#> Lactobacillus_crispatus__Gardnerella_vaginalis 41.7 54.2 0.0 0.0 0.0
#> Lactobacillus_crispatus__Lactobacillus_iners 44.4 51.1 0.0 0.0 4.5
#> Lactobacillus_crispatus__Lactobacillus_jensenii 82.0 18.0 0.0 0.0 0.0
#> Lactobacillus_crispatus__Lactobacillus_reuteri 74.2 25.8 0.0 0.0 0.0
#> Lactobacillus_gasseri 0.0 0.0 98.4 0.0 0.0
#> Lactobacillus_iners 0.0 0.0 0.0 4.5 18.2
#> Lactobacillus_iners__g_Corynebacterium_1 0.0 0.0 0.0 91.3 8.7
#> Lactobacillus_iners__g_Finegoldia 0.0 0.0 0.0 81.6 18.4
#> Lactobacillus_iners__g_Streptococcus 0.0 0.0 0.0 67.7 29.0
#> Lactobacillus_iners__Gardnerella_vaginalis 0.0 0.0 0.0 39.0 51.0
#> Lactobacillus_iners__Lactobacillus_crispatus 0.0 0.0 0.0 48.7 51.3
#> Lactobacillus_iners__Lactobacillus_gasseri 0.0 0.0 5.0 80.0 15.0
#> Lactobacillus_iners__Lactobacillus_jensenii 0.0 0.0 0.0 77.6 13.1
#> Lactobacillus_jensenii__Gardnerella_vaginalis 0.0 0.0 0.0 0.0 0.0
#> Lactobacillus_jensenii__Lactobacillus_iners 0.0 0.0 0.0 0.0 0.0
#> Sneathia_sanguinegens__Gardnerella_vaginalis 0.0 0.0 0.0 0.0 2.5
#> Valencia
#> dCST IV-A IV-B IV-C0 IV-C1 IV-C2
#> Atopobium_vaginae__Gardnerella_vaginalis 3.9 92.2 0.0 0.0 0.0
#> BVAB1__Gardnerella_vaginalis 100.0 0.0 0.0 0.0 0.0
#> g_Streptococcus 0.0 0.0 9.0 58.2 3.0
#> Gardnerella_vaginalis 10.0 46.7 23.3 0.0 0.0
#> Gardnerella_vaginalis__Atopobium_vaginae 0.0 99.3 0.0 0.0 0.0
#> Gardnerella_vaginalis__BVAB1 75.8 24.2 0.0 0.0 0.0
#> Gardnerella_vaginalis__g_Fastidiosipila 3.4 96.6 0.0 0.0 0.0
#> Gardnerella_vaginalis__Lactobacillus_iners 1.7 94.2 0.0 0.0 0.0
#> Gardnerella_vaginalis__Sneathia_sanguinegens 2.4 97.6 0.0 0.0 0.0
#> Lactobacillus_crispatus__g_Corynebacterium_1 0.0 0.0 0.0 0.0 0.0
#> Lactobacillus_crispatus__g_Finegoldia 0.0 0.0 0.0 0.0 0.0
#> Lactobacillus_crispatus__g_Lactobacillus 0.0 0.0 0.0 0.0 0.0
#> Lactobacillus_crispatus__Gardnerella_vaginalis 4.2 0.0 0.0 0.0 0.0
#> Lactobacillus_crispatus__Lactobacillus_iners 0.0 0.0 0.0 0.0 0.0
#> Lactobacillus_crispatus__Lactobacillus_jensenii 0.0 0.0 0.0 0.0 0.0
#> Lactobacillus_crispatus__Lactobacillus_reuteri 0.0 0.0 0.0 0.0 0.0
#> Lactobacillus_gasseri 0.0 0.0 0.0 0.0 1.6
#> Lactobacillus_iners 4.5 13.6 27.3 0.0 9.1
#> Lactobacillus_iners__g_Corynebacterium_1 0.0 0.0 0.0 0.0 0.0
#> Lactobacillus_iners__g_Finegoldia 0.0 0.0 0.0 0.0 0.0
#> Lactobacillus_iners__g_Streptococcus 0.0 0.0 0.0 0.0 0.0
#> Lactobacillus_iners__Gardnerella_vaginalis 1.2 8.8 0.0 0.0 0.0
#> Lactobacillus_iners__Lactobacillus_crispatus 0.0 0.0 0.0 0.0 0.0
#> Lactobacillus_iners__Lactobacillus_gasseri 0.0 0.0 0.0 0.0 0.0
#> Lactobacillus_iners__Lactobacillus_jensenii 0.0 0.0 0.0 0.0 0.0
#> Lactobacillus_jensenii__Gardnerella_vaginalis 0.0 0.0 0.0 0.0 0.0
#> Lactobacillus_jensenii__Lactobacillus_iners 0.0 0.0 0.0 0.0 0.0
#> Sneathia_sanguinegens__Gardnerella_vaginalis 15.0 82.5 0.0 0.0 0.0
#> Valencia
#> dCST IV-C3 IV-C4 V
#> Atopobium_vaginae__Gardnerella_vaginalis 0.0 0.0 3.9
#> BVAB1__Gardnerella_vaginalis 0.0 0.0 0.0
#> g_Streptococcus 28.4 1.5 0.0
#> Gardnerella_vaginalis 3.3 16.7 0.0
#> Gardnerella_vaginalis__Atopobium_vaginae 0.0 0.0 0.0
#> Gardnerella_vaginalis__BVAB1 0.0 0.0 0.0
#> Gardnerella_vaginalis__g_Fastidiosipila 0.0 0.0 0.0
#> Gardnerella_vaginalis__Lactobacillus_iners 0.0 0.0 0.0
#> Gardnerella_vaginalis__Sneathia_sanguinegens 0.0 0.0 0.0
#> Lactobacillus_crispatus__g_Corynebacterium_1 0.0 0.0 0.0
#> Lactobacillus_crispatus__g_Finegoldia 0.0 0.0 0.0
#> Lactobacillus_crispatus__g_Lactobacillus 0.0 0.0 0.0
#> Lactobacillus_crispatus__Gardnerella_vaginalis 0.0 0.0 0.0
#> Lactobacillus_crispatus__Lactobacillus_iners 0.0 0.0 0.0
#> Lactobacillus_crispatus__Lactobacillus_jensenii 0.0 0.0 0.0
#> Lactobacillus_crispatus__Lactobacillus_reuteri 0.0 0.0 0.0
#> Lactobacillus_gasseri 0.0 0.0 0.0
#> Lactobacillus_iners 18.2 4.5 0.0
#> Lactobacillus_iners__g_Corynebacterium_1 0.0 0.0 0.0
#> Lactobacillus_iners__g_Finegoldia 0.0 0.0 0.0
#> Lactobacillus_iners__g_Streptococcus 0.0 0.0 3.2
#> Lactobacillus_iners__Gardnerella_vaginalis 0.0 0.0 0.0
#> Lactobacillus_iners__Lactobacillus_crispatus 0.0 0.0 0.0
#> Lactobacillus_iners__Lactobacillus_gasseri 0.0 0.0 0.0
#> Lactobacillus_iners__Lactobacillus_jensenii 0.0 0.0 9.3
#> Lactobacillus_jensenii__Gardnerella_vaginalis 0.0 0.0 100.0
#> Lactobacillus_jensenii__Lactobacillus_iners 0.0 0.0 100.0
#> Sneathia_sanguinegens__Gardnerella_vaginalis 0.0 0.0 0.0The depth-2 refinement recovers sub-CST structure. For example, the Lactobacillus crispatus dCST splits based on the second-most-abundant taxon, and these sub-dCSTs align with Valencia sub-CSTs I-A and I-B.
For convenience, linf.dcst.landmark.pipeline() runs the
entire workflow in one call: normalization, depth-1 dCSTs, depth-2
refinement, and landmark computation.
out <- linf.dcst.landmark.pipeline(
valencia2k$rel,
feature.ids = colnames(valencia2k$rel),
feature.labels = colnames(valencia2k$rel),
n0.depth1 = 30,
n0.depth2 = 15,
refinement.factor = 2,
low.freq.policy = "pure",
landmark.view = "absorb",
verbose = FALSE
)
names(out)
#> [1] "linf.rel" "dcst.depth1" "dcst.depth2" "landmarks.depth1"
#> [5] "landmarks.depth2" "params"All four landmark methods select observed rows by the lineage’s leaf-feature value: its maximum, minimum, or the observation nearest its mean or median. They do not construct averaged community profiles. The pipeline supplies L-infinity-normalized values, so these measurements are relative to the sample maximum, not proportions of total community abundance.
## Depth-1 landmarks
lm1 <- out$landmarks.depth1$landmarks
## Show landmarks for a few key dCST dominance-lineages
key_lineages <- c("Lactobacillus_crispatus", "Lactobacillus_iners",
"Gardnerella_vaginalis")
key_lm <- lm1[lm1$lineage.id %in% key_lineages, ]
## For each dCST, show the endpoint.max landmark's target value
## (target-feature abundance relative to the row maximum)
ep_max <- key_lm[key_lm$landmark.type == "endpoint.max",
c("lineage.label", "point.name", "target.value")]
ep_max
#> lineage.label point.name target.value
#> 1 Lactobacillus_crispatus s1 1
#> 5 Gardnerella_vaginalis s6 1
#> 9 Lactobacillus_iners s7 1
## And the endpoint.min (lowest target value among assigned rows)
ep_min <- key_lm[key_lm$landmark.type == "endpoint.min",
c("lineage.label", "point.name", "target.value")]
ep_min
#> lineage.label point.name target.value
#> 2 Lactobacillus_crispatus s658 0.00504737
#> 6 Gardnerella_vaginalis s577 0.01120848
#> 10 Lactobacillus_iners s312 0.00000000A depth-1 pure dominant feature has normalized value one in every positive member, so endpoint selection can consist entirely of ties. Absorbed samples may have a smaller target value because their assigned feature is not their original maximum. Neither endpoint establishes near-total or marginal unit-sum dominance. Inspect unit-sum relative abundances for that question.
This workflow fits supported dominance groups, refines selected
parents and selects observed landmark rows. Its concordance tables
describe overlap with Valencia labels on source training samples. To
assign separate samples while preserving the fitted states, use
transfer.dcsts() and inspect unassigned (NA)
depths; fitting on new samples would learn a different hierarchy. The function guide explains these contracts
and the dataset
guide gives a disjoint reference/query demonstration.