Start with the result you need: a dominant feature for each sample, a supported grouping of samples, a deeper hierarchy, or assignments to a reference you have already fitted. These are different stopping points. You do not need to run the full pipeline to use L-infinity normalization.
The example datasets guide catalogs the data separately. For longer analyses, see the normalization and gut tutorial and vaginal microbiome tutorial.
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.
| Your task | Entry point | When to stop |
|---|---|---|
| Remove low-depth samples and low-prevalence features from counts | filter.asv() |
You need filtered counts or unit-sum relative abundances. |
| Express abundance relative to each sample’s largest feature | normalize.linf() |
You need scaled profiles, without grouping samples. |
| Name the largest feature in each sample | linf.dominant.features() |
You need individual assignments, without a support threshold. |
| Retain supported dominance groups | linf.csts() |
A depth-1 dCST partition answers your question. |
| Subdivide selected groups by remaining features | refine.linf.csts() |
The chosen depth is sufficient; deeper is not automatically better. |
| Inspect stored low-support policies or select representatives | dcst.view(), summary(),
linf.landmarks() |
You need a descriptive summary of the fitted samples. |
| Assign new samples to an existing hierarchy | transfer.dcsts() |
You need frozen-reference assignments, without learning new states. |
| Prepare coordinate charts or readable names | linf.hypercube.embedding(),
linf.feature.labels() |
You need a representation for display or further analysis. |
These six samples have stable IDs that differ from the display names. Two depth-1 groups have support at least two. One parent can be split into two supported children.
X <- rbind(
s1 = c(10, 8, 1), s2 = c(9, 7, 2),
s3 = c(10, 1, 8), s4 = c(9, 2, 7),
s5 = c(1, 10, 2), s6 = c(2, 9, 1)
)
colnames(X) <- c("asv_1", "asv_2", "asv_3")
labels <- c("Taxon A", "Taxon B", "Taxon C")
M <- normalize.linf(X)
dominant <- linf.dominant.features(M, feature.labels = labels)
dominant$id
#> [1] "asv_1" "asv_1" "asv_1" "asv_1" "asv_2" "asv_2"
#> asv_1
#> asv_1
#> asv_1
#> asv_1
#> asv_2
#> asv_2
fit1 <- linf.csts(M, feature.labels = labels, n0 = 2,
low.freq.policy = "pure")
fit2 <- refine.linf.csts(M, fit1, n0 = 2, refinement.factor = 2,
low.freq.policy = "pure", verbose = FALSE)
accounting <- summary(fit2)
fields <- c("depth", "n.lineages", "input.samples",
"assigned.samples", "unassigned.samples", "rare.samples")
knitr::kable(accounting[, fields],
col.names = c("Depth", "Groups", "Input", "Assigned", "Unassigned", "Rare"))| Depth | Groups | Input | Assigned | Unassigned | Rare |
|---|---|---|---|---|---|
| 1 | 2 | 6 | 6 | 0 | 0 |
| 2 | 3 | 6 | 6 | 0 | 0 |
Stop at M for normalized profiles or at
dominant for sample-level maximum features.
fit1 adds the support threshold; fit2 adds a
level of subdivision. The two registered methods work through these
objects: print(fit2) displays counts at each active depth,
while summary(fit2) returns a data frame of lineage counts
and sizes. The table accounts for all six samples at both depths: two
groups become three after refinement. Summaries include separate input,
assigned, unassigned and rare sample counts. Size statistics include
rare groups and are NA when no samples are assigned,
without warnings. For compatibility, total.samples
continues to mean assigned samples; use input.samples for
the full denominator.
On narrow screens, focus a table and scroll horizontally to read all columns. Function names remain intact for copying.
This guide describes the 0.3.1 interface: 13 explicit
function exports, two registered S3 methods, and five bundled
datasets. S3 methods are selected by R when you call
print() or summary() on a fitted object.
Neither method is also an explicit function export, so there is no
overlap in these counts.
Each explicit export appears once in the catalog below. Use
help("function.name", package = "linf") for complete
arguments and examples, for example
help("normalize.linf", package = "linf").
| Function | Purpose | Expected input | Principal result |
|---|---|---|---|
filter.asv() |
Filter library size and feature prevalence. | Dense samples-by-features counts; numeric data frames also accepted. | List with counts, unit-sum rel, kept
row/column indices, prevalence and thresholds. |
normalize.linf() |
Divide rows by their maximum above tol. |
Finite, nonnegative dense or sparse matrix. | Same-sized matrix; zero and below-tolerance rows remain unchanged. |
linf.dominant.features() |
Assign each sample’s maximum feature. | Finite, nonnegative matrix and optional aligned IDs/labels. | Indices, IDs, labels and observed/full feature levels; optional row maxima. |
| Function | Purpose | Expected input | Principal result |
|---|---|---|---|
linf.csts() |
Retain dominance sample sets with support at least
n0. |
Samples-by-features matrix, usually L-infinity normalized. | Class linf.csts, with active, pure and absorb
hierarchies. |
linf.udcsts() |
Pool top-feature orders into unordered sets before applying support. | Nonnegative samples-by-features matrix and a level. | Pure set IDs and frequencies, assignments, and a sample mask; pure, absorb or filter policy. |
linf.udcst.graph() |
Construct pair-state graphs supported by observed triples. | Matrix or matching pure pair/triple fits; coverage or support selection. | Nodes, unit edges, full incidence, witness similarities, faces, sample membership and components. |
refine.linf.csts() |
Add one hierarchy level within selected active leaf lineages. | Fitted object and its matrix in the original row/column order. | Updated linf.csts; previous levels and feature metadata
retained. |
dcst.view() |
Activate a stored policy without recomputing it. | Fitted object; view = "pure" or
"absorb". |
Copy with active assignments switched and attached landmarks cleared. |
linf.landmarks() |
Select observed samples by the lineage’s leaf-feature value. | Fitted object, aligned matrix, depth and view. | lineages metadata and landmarks selection
table, with row indices/names. |
linf.dcst.landmark.pipeline() |
Normalize, fit depth 1, refine once and select landmarks. | Nonnegative matrix, feature metadata and stage-specific controls. | Normalized matrix, two fitted objects, two landmark objects and parameters. |
| Function | Purpose | Expected input | Principal result |
|---|---|---|---|
transfer.dcsts() |
Walk new samples through a frozen fitted hierarchy. | Query matrix, reference fit, feature-matching and view controls. | Label/ID assignment matrices, feature matches and stopping
diagnostics; unassignable depths are NA. |
linf.hypercube.embedding() |
Construct a zero-aware chart relative to one component. | Nonnegative matrix with at least two features and a reference index, ID or label. | Dense matrix with one fewer column; attributes record the reference and scaling. |
linf.feature.labels() |
Format taxonomy for display while keeping separate stable IDs. | Equal-length feature-ID and taxonomy vectors; optional aliases/abbreviations. | Unique display labels. |
Rows are samples and columns are
features. Give samples unique row names and features
unique, nonmissing stable IDs. Keep taxonomy or other display text in a
separate vector. feature.ids and
feature.labels are positional: each must have length
ncol(X) and describe columns in exactly that order.
Metadata vector names do not perform a join. Defaults use column names
as IDs, or V1, V2, … for unnamed columns, and
use IDs as display labels. Fitting disambiguates duplicates with
make.unique(..., sep = "_"); supplying unique IDs from the
start avoids an order-dependent mapping.
For refinement and landmarks, reuse the fitted matrix’s row
and column order. Both dimensions are checked. New fits save
input.dimnames separately from custom feature IDs; if an
axis is named in both the fit and the supplied matrix, its names/order
must match. The error identifies the axis to align. There is no
automatic reordering. Unnamed axes and older fits without saved input
names retain the positional contract; prefer unique names because
permutations within duplicated names cannot be detected. Align sample
metadata with match(rownames(X), metadata$sample_id) and
verify all matches before analysis. After filtering, use
kept.sample.idx and kept.feature.idx to subset
metadata. See the companion’s alignment
example.
Core matrix functions accept ordinary dense numeric matrices and
Matrix sparse matrices. backend = "auto"
normally preserves sparse input; refinement and landmarks inherit the
fitted backend when their backend argument is omitted. Explicit
"dense" and "sparse" choose a conversion path.
Sparse output is numeric compressed-column storage. Embeddings always
return dense coordinates. filter.asv() is a separate dense
count-filtering interface; it does not accept a sparse
Matrix object directly.
linf.udcsts() groups rows by their highest-abundance
feature sets, ignoring order. At level 1, its pure membership agrees
with dominant-feature grouping. At level 2, (A, B) and
(B, A) pool before applying
n0. Calls at different levels are separate partitions, not
necessarily a refinement hierarchy.
U <- rbind(c(A = 9, B = 5, C = 1), c(A = 8, B = 4, C = 1),
c(A = 4, B = 9, C = 1), c(A = 3, B = 8, C = 1),
c(A = 1, B = 3, C = 9))
unordered <- linf.udcsts(U, level = 2, n0 = 3, low.freq.policy = "filter")
unordered$frequency.table
#> ID freq cumm.pct
#> 1 1,2 4 80
#> 2 2,3 1 100
U[unordered$keep, , drop = FALSE]
#> A B C
#> [1,] 9 5 1
#> [2,] 8 4 1
#> [3,] 4 9 1
#> [4,] 3 8 1
unordered$feature.labels[unordered$feature.sets[1, ]]
#> [1] "A" "B"
#> A
#> BFour rows belong to the pure set {A, B} and remain; the
single {B, C} row is filtered. pure.id always
retains every row’s original set. ID uses original column
indices, freq counts rows before filtering, and
cumm.pct is the percentage of all input
rows in states with at least the current row’s frequency.
Equal-size states have equal cumulative percentages.
The "pure" policy marks small states as rare while
keeping their rows; "filter" excludes them through
keep without reassignment. Both use missing active
assignments for those rows and retain their original
pure.id. "absorb" assigns a rare row to the
retained set with greatest combined feature abundance. This set-based
rule differs from ordered parent-local dCST absorption. Numerical tuple
order resolves tied candidate scores. Absorption errors when no state
survives; filtering may legitimately keep no rows. Subset all metadata
with the same mask.
This constructor always uses original-column ties, rejects all-zero
rows, and reports positive.features so that sets containing
zero-valued ranks can be identified. It accepts counts or proportions
without normalization. The output is not a linf.csts
hierarchy and is not an input for its refinement, landmark, policy-view
or frozen-transfer functions. See ?linf.udcsts for the full
result fields and sparse-input behavior.
Count filtering removes samples/features using library size and
prevalence. filter.asv() applies the library-size threshold
first, computes prevalence among those retained samples, then drops
samples left with no counts. Its rel matrix sums to one
across the retained features in each row.
For a nonnegative row, L-infinity normalization instead divides by
its largest entry when that maximum exceeds tol:
\[ M_{ij} = X_{ij}/\max_k X_{ik}\quad\text{when }\max_k X_{ik}>\mathrm{tol}. \]
It is neither unit-sum normalization nor a log-ratio transform. No
pseudocount is added. At the default tol = 0, a positive
row has maximum one; an all-zero row remains zero. With positive
tolerance, a row whose maximum is at or below the
tolerance is left unchanged, including its small nonzero entries. Such a
row still has a dominant feature. Only exact-zero rows receive
NA from linf.dominant.features().
The core matrix functions reject missing (NA,
NaN), infinite and negative entries and empty matrices;
they do not impute missing values. filter.asv() also
validates finite nonnegative counts and threshold ranges. Fractional
count-like values are accepted. Empty filtering results keep every
documented field and indices that correspond to the returned matrices.
With min.lib = 0, zero-total rows count toward prevalence
but have no present features under a relative threshold; they are
removed before returning rel.
Dominant-feature assignment and refinement depend on within-row rankings. Positive row scaling preserves those rankings. L-infinity scaling is useful for interpreting feature values but is not required to discover the maximum. The retained feature set, column order for ties, support thresholds and policy still affect the resulting grouping.
The default tie.method = "first" selects the first
exactly maximal column. "random" samples among exact ties
using R’s current random-number state; these functions have no private
seed and do not restore the caller’s state. Set set.seed()
immediately before a reproducible random-tie call. Dense and sparse
paths use the same column ordering for dominance and positive absorption
ties. "error" stops at a positive tie. Zero rows do not
trigger a dominance tie.
Refinement has no tie.method argument:
its child fits use "first", even if the parent was fitted
with random ties. The pipeline’s tie argument controls depth-1 fitting
and landmark selection, not refinement. Landmark "first"
selects the first tied sample in matrix row order. Frozen transfer has
its own default, "support", described below.
A dominant feature is one sample’s largest feature.
Samples sharing it form a provisional dominance sample
set. A set retained at support n0 becomes a named
depth-1 dCST. A dominance lineage records successive
feature choices as a group is refined. Depth counts
stored hierarchy levels; it need not equal the number of features in
every terminal lineage.
Under the pure policy, low-support samples enter a
synthetic rare bucket (default RARE_DOMINANT). Under the
absorb policy, they are reassigned to the retained
feature with greatest abundance in that sample. If every retained
feature is zero, fitting falls back to the state with greatest reference
support, then lexical label order, regardless of tie method. Exact-zero
rows also follow that fitting fallback. With no retained states, pure
assignments are all rare and absorb assignments are all
NA.
Both representations are stored. Unsuffixed fields are the
active view:
lineage.id/lineage.label hold current leaves;
lineage.ids/lineage.labels hold a list of
levels. Their .pure and .absorb counterparts
preserve stored policies. dcst.view(fit2, "absorb") changes
which stored hierarchy is active without fitting again, and clears
attached landmarks so that summaries from a different view are not
reused accidentally. size.table describes provisional
depth-1 support, not current leaf sizes.
With lineages.to.refine = NULL, automatic selection uses
active leaf support at least
refinement.factor * n0 (defaults: 2 * 50). An
explicit character vector selects existing active leaf
IDs, bypassing that parent size rule; it does not
bypass the child support threshold. Unknown IDs and the top-level
synthetic rare ID are rejected. For example:
explicit <- refine.linf.csts(
M, fit1, lineages.to.refine = "asv_1", n0 = 2,
low.freq.policy = "pure", verbose = FALSE
)
stopifnot(identical(explicit$lineage.ids, fit2$lineage.ids))Within each selected active group, refinement removes features already used on its fitted path and fits the remaining columns. It appends a stored level on every call. Parents with no retained child, no remaining feature, or no selection keep their labels at that level. Repeated calls extend the hierarchy; they do not guarantee an additional feature for every sample. Parent-prefixed rare buckets at deeper levels are stored synthetic categories, not features.
Selection and child fitting use the active membership. The saved
policy views are representations of that fitted history, not two
independently refitted analyses. Choose the active policy before
refining and keep low.freq.policy, rare.label
and sep consistent across calls. Fitted objects store
explicit feature paths and node keys, including IDs containing
__. A real A__B feature and the refined path
A then B are different nodes. When their
readable strings collide, returned IDs/labels receive a
[node ...] suffix; select the returned
lineage.id when refining. A real feature named
RARE_DOMINANT is distinct from a synthetic rare node.
Synthetic or undefined parents stay terminal in their own policy view.
Unambiguous older fits are upgraded on use; ambiguous legacy fits must
be rebuilt from their matrices. Never split displayed strings to recover
features.
landmarks <- linf.landmarks(M, fit2, view = "absorb",
landmark.types = c("endpoint.min", "mean.rep"))
knitr::kable(landmarks$landmarks[, c(
"lineage.label", "landmark.type", "point.name", "observed.value",
"target.value"
) ], digits = 3)| lineage.label | landmark.type | point.name | observed.value | target.value |
|---|---|---|---|---|
| Taxon A__Taxon B | endpoint.min | s2 | 0.778 | 0.778 |
| Taxon A__Taxon B | mean.rep | s1 | 0.800 | 0.789 |
| Taxon A__Taxon C | endpoint.min | s4 | 0.778 | 0.778 |
| Taxon A__Taxon C | mean.rep | s3 | 0.800 | 0.789 |
| Taxon B | endpoint.min | s5 | 1.000 | 1.000 |
| Taxon B | mean.rep | s5 | 1.000 | 1.000 |
# Retrieve the complete observed profiles, if needed:
profiles <- M[landmarks$landmarks$point.index, , drop = FALSE]Landmarks target the leaf feature of each lineage at the requested
depth. endpoint.max and endpoint.min select
rows with the largest and smallest target-feature value.
mean.rep and median.rep select the
observed row closest to the mean or median of that one
feature. None constructs a mean profile or a centroid across
features. Several methods may choose the same row.
Values are on the scale of the supplied matrix. With M
above they measure abundance relative to each sample’s row maximum, not
a fraction of total abundance. At depth 1 under the pure policy,
normalized dominant values are usually all one, so endpoint methods can
reduce to tied selections. Under absorption the target need not be the
sample’s true maximum. These summaries alone do not establish
whole-community typicality or biological distinctness.
Inspect landmarks$lineages$landmarks.computable: rare
categories have no unique target and are skipped. Target IDs come from
explicit node paths, so separators and colliding readable paths cannot
change the feature selected. Terminal lineages retain their last feature
at later stored depths. Keep stable IDs and display labels separate;
neither display labels nor concatenated lineage strings should be split
to infer feature identities.
out <- linf.dcst.landmark.pipeline(
X, feature.labels = labels, n0.depth1 = 2, n0.depth2 = 2,
refinement.factor = 2, verbose = FALSE
)
names(out)
#> [1] "linf.rel" "dcst.depth1" "dcst.depth2" "landmarks.depth1"
#> [5] "landmarks.depth2" "params"
#> linf.rel
#> dcst.depth1
#> dcst.depth2
#> landmarks.depth1
#> landmarks.depth2
#> params
stopifnot(identical(out$dcst.depth2$lineage.ids, fit2$lineage.ids))Defaults are n0.depth1 = 50,
n0.depth2 = 25, refinement.factor = 2,
low.freq.policy = "pure",
landmark.view = "absorb",
tie.method = "first", sep = "__", and
backend = "auto". All four landmark types are requested at
both depths. The wrapper normalizes with default tolerance zero; it does
not filter counts. It returns linf.rel,
dcst.depth1, dcst.depth2,
landmarks.depth1, landmarks.depth2 and
params. Use explicit stages when you want another
tolerance, selected lineages, more depths, or fewer results.
transfer.dcsts() assigns new rows to realized reference
lineages, without changing their features, support counts or structure.
It defaults to the absorb view, matching by
feature IDs, and all fitted depths. Columns may be
reordered; extra features are ignored and missing reference features are
filled with zero. Pass query feature.ids in query-column
order when column names are not those IDs. Label matching is optional.
Unnamed matrices use synthetic V1, V2, … IDs,
which require consistent positional meanings. Matching keys must be
unique, nonmissing and nonempty. Duplicate keys are rejected; choose an
explicit biological aggregation or disambiguation upstream. Repeated
display labels remain allowed when matching by unique feature IDs.
At each depth, the query chooses among realized children using the
abundance of the newly added feature. The default
tie.method = "support" resolves positive ties using
reference support, then lexical order. "first" uses frozen
child order, not query-column order; "random" uses the
current R seed; "error" stops on ties. A terminal label can
recur only where it is actually stored, scored using its last feature.
With no candidate or no positive candidate feature, this depth and
subsequent depths are NA. There is no fitting-style
zero-evidence fallback, even under
view = "absorb". carry.forward.terminal.depths
is retained for call compatibility; neither setting permits assignments
outside the fitted hierarchy.
The original assignment matrices contain display labels even when
matching by IDs. Their assignment.ids and
all.depths.ids counterparts contain lineage IDs for joins
using identifiers that do not encode display labels. The existing
lexical display-label tie rule still applies: relabeling and refitting
can change tied assignments even though the identifiers themselves are
unchanged. assignment contains requested depths and
all.depths contains all fitted depths. A pure rare bucket
has no feature to score; it is not a learned query class. Reference
fitting and frozen transfer thus need not reproduce every training
assignment, especially for rare, zero or tied rows. The companion
includes a separate
reference/query example.
query <- M[c("s1", "s5"), c("asv_2", "asv_1"), drop = FALSE]
transferred <- transfer.dcsts(query, fit2, view = "pure")
transferred$assignment.ids
#> depth1 depth2
#> s1 "asv_1" "asv_1__asv_2"
#> s5 "asv_2" "asv_2"
#> asv_1
#> asv_2
#> asv_1__asv_2
#> asv_2
knitr::kable(transferred$feature.match)| feature.id | feature.label | query.index | matched |
|---|---|---|---|
| asv_1 | Taxon A | 2 | TRUE |
| asv_2 | Taxon B | 1 | TRUE |
| asv_3 | Taxon C | NA | FALSE |
| sample.id | assigned.depth | reason |
|---|---|---|
| s1 | 2 | complete |
| s5 | 2 | complete |
fit2$history[[2]][c("n0", "selection.mode", "selected.lineage.ids", "tie.method")]
#> $n0
#> [1] 2
#>
#> $selection.mode
#> [1] "automatic"
#>
#> $selected.lineage.ids
#> [1] "asv_1"
#>
#> $tie.method
#> [1] "first"
#> automatic
#> asv_1
#> firstThe feature-match table shows that asv_3 is absent, even
if both queries can complete this particular walk through other
features. For an unassigned query,
missing_candidate_features means every real candidate at
the stopping depth was absent; no_positive_candidate_values
means some candidates were present but none was positive. Check the
present/missing candidate counts if some were absent.
no_candidates and synthetic_only describe the
frozen reference. These diagnostics describe every fitted depth even
when fewer are requested. They explain the rule applied, not confidence
or predictive accuracy.
The history list records the support threshold,
selection, source/returned policy and tie handling for every new depth.
refined.node.ids excludes selected parents that stayed
terminal because no child could be retained. Switching views preserves
this history. The top-level n0 remains the depth-1 setting;
use the corresponding history entry for a later depth. Older fits can
still be used; refining one marks previously unsaved history entries
available = FALSE.
linf.hypercube.embedding(X, reference) needs a finite
nonnegative matrix and one unambiguous reference component. For positive
reference entries it forms ratios to that component, then maps their
direction radially into the unit cube. For a zero reference it uses the
L-infinity boundary extension; an all-zero sample maps to zero by
convention. Its tolerance controls which reference entries and norms are
treated as zero, unlike the leave-unchanged tolerance rule of
normalize.linf().
With p input features it returns a dense
samples-by-(p − 1) matrix, plus attributes recording reference
identity, other feature IDs, lambda and
finite/zero-reference row counts. By default lambda is
scaled so the radial map reaches 0.95 at the 0.95 quantile of positive
reference-ratio norms (fallback scale one if none exceed tolerance).
Logarithmic calculations include norms outside the ordinary numeric
range without overflow. For comparable charts of reference and query
data, reuse the reference choice and
log.lambda = attr(reference.embedding, "log.lambda"). The
ordinary lambda attribute is NA if that scale
is not representable as a positive finite number; the logarithmic value
remains usable. -expm1(-t) preserves small positive radii.
The result is dense by implementation choice; non-reference zeros stay
zero.
This is a coordinate representation, not a clustering or predictive fit. Distances and apparent separation depend on the feature subset, reference and scaling; coordinates alone do not demonstrate biological groups or validated prediction. The companion shows a static chart. The optional interactive embedding article is website-only and is not an installed vignette.
Transfer and landmark calculations reuse prepared candidate positions
and memberships. Embedding still returns a dense matrix even for sparse
input; its numeric output alone occupies about
8 * nrow(X) * (ncol(X) - 1) bytes, before temporary
storage. The installed performance note gives measured workloads,
timings and memory limits of the comparison:
help(package = "linf")
help("linf-package", package = "linf")
browseVignettes("linf")
vignette("example-datasets", package = "linf")Use lineage.id/lineage.label for leaf
assignments and lineage.ids/lineage.labels for
levels. The 0.2 release introduced these explicit policy hierarchies;
rebuild pre-0.2 flat objects from their matrices. Use
linf.dominant.features() for depth-1 assignment and
dcst.view() for policy selection. Since 0.3, repeat
refine.linf.csts() for additional depths, use the explicit
filtering/normalization/fitting stages, and format summary tables with
ordinary reporting tools. Removed cell-based aliases and former
convenience functions are not part of the current API.
The organization follows the short purpose descriptions and help
cross-references in Hmisc’s
documentation and package overview,
adapted to these few workflows. Maintainers can run
make audit-guides to check the catalog against
NAMESPACE, registered methods, help aliases and bundled
data; vignette builds execute the examples independently.
linf.udcst.graph() constructs pure level-two state
graphs. Both endpoint pair configurations must occur within their common
leading triple. Counts always use the full input sample set, including
pair states excluded by the coverage rule.
G <- rbind(c(A=6, B=3, C=1), c(A=6, B=1, C=3), c(A=1, B=6, C=3))
graph <- linf.udcst.graph(G, coverage=60, min.support=1)
graph$nodes
#> ID freq cumm.pct node label feature.a feature.b component
#> 1 1,2 1 100 1 A + B 1 2 1
#> 2 1,3 1 100 2 A + C 1 3 1
#> 3 2,3 1 100 3 B + C 2 3 1
graph$edges[, c("pair.a", "pair.b", "triple", "min.support", "length")]
#> pair.a pair.b triple min.support length
#> 1 1,2 1,3 1,2,3 1 1
#> 2 1,2 2,3 1,2,3 1 1
#> 3 1,3 2,3 1,2,3 1 1
graph$faces
#> triple a b c min.support n.triple
#> 1 1,2,3 1 2 3 1 3Coverage retains tied frequency groups together, so actual coverage
can exceed the target. adjacency="shared_feature" provides
a comparison without requiring both observed configurations. Increasing
min.support removes edges while preserving isolated
vertices. Each edge has unit path length; support and Jaccard
similarities are separate attributes. A filled face requires all three
pairs within a single witness triple. The function does not embed or
repair the graph; use an embedding package on within-component graph
distances.