Interpretation#
The Atacama chapters of Tier 2 have produced four results: a sparse network at \(\lambda_1 = 0.8\) (Selecting lambda), a sparse + low-rank decomposition at \(\mu_1 = 15\) (Choosing the Latent Rank), a reading of what the latent axes correspond to (Latent Components & Covariates), and a set of cross-validated log-contrast models, one per environmental outcome (Log-Contrast Models at Scale). Each limits how you can read the others.
Note
Which numbers are verified. The recompute ran under QIIME 2 2026.7 but did not cover everything, so the claims below are not uniformly solid:
quantity |
status |
|---|---|
\(\lambda = 0.8\), 216 edges, eBIC 16130.0988 |
reproduced through the CLI |
\(\mu_1 \rightarrow\) rank map, and the edge/node counts at each rank |
reproduced |
taxonomy of individual nodes |
resolves for all 300 features since the bundle was rebuilt with real feature IDs |
correlations between components and covariates |
not re-run |
\(R^2\) values |
not re-run — the recompute reports cross-validated error; converting it needs a held-out |
named taxa in the narrative below |
carried over from the reference analysis |
Read the unverified rows as “how to read this number once you have it”, not as a report of it.
The two blocks are one model#
It is tempting to treat \(\hat{\Theta}_S\) as “the network” and \(\hat{L}\) as “the noise we removed”. That is the wrong split. The model is
and the two terms are estimated jointly, competing for the same covariance [M6, M7]. Neither is meaningful unless the other is stated.
This changes what an edge is. In the SGL network of Selecting lambda, a nonzero \(\hat{\Theta}_{ij}\) means taxa \(i\) and \(j\) are conditionally dependent given the other 298 taxa in the table. In the SLR network it means they are conditionally dependent given the other 298 taxa and given the \(r\) latent directions. Those are different claims about the world, and the SLR edge is the stronger of the two.
Symmetrically, the loadings of \(\hat{L}\) are not a nuisance parameter. A taxon with a large loading on a latent component is one whose abundance is largely explained by a global gradient shared across the community, and it may carry very few edges precisely because the latent block already accounts for most of its covariance. Low degree in \(\hat{\Theta}_S\) plus high loading in \(\hat{L}\) is an interpretable profile: a generalist responding to the environment rather than to its neighbours. Read only the sparse block and such a taxon looks uninteresting.
Reading a taxon’s neighbourhood#
Interpret one taxon and its neighbours rather than the whole 300-node graph. At
this size the full picture is a hairball, and the summarize heatmap is for
spotting block structure, not for reading individual relationships.
Export the solution, convert the precision matrix to partial correlations, and join the taxonomy:
qiime tools export \
--input-path atacama-top-300-slr-lambda0.8-rank2.qza \
--output-path slr-rank2-export
qiime tools export \
--input-path atacama-top-300-clr.qza \
--output-path clr-export
qiime tools export \
--input-path atacama-taxonomy-silva138.qza \
--output-path taxonomy-export
import biom
import numpy as np
import pandas as pd
import zarr
store = zarr.ZipStore("slr-rank2-export/problem.zip", mode="r")
root = zarr.open(store=store)
Theta = np.asarray(root["solution/precision_"])
# partial correlations: -Theta_ij / sqrt(Theta_ii * Theta_jj)
d = np.sqrt(np.diag(Theta))
P = -Theta / np.outer(d, d)
np.fill_diagonal(P, 1.0)
# feature IDs, in the order used to build the covariance matrix:
# the solution artifact stores them in an ordered `labels/` group
ids = [str(root[f"labels/{i}"][()]) for i in range(Theta.shape[0])]
P = pd.DataFrame(P, index=ids, columns=ids)
# cross-check only: the transformed table should agree, in the same order
X = biom.load_table("clr-export/feature-table.biom").to_dataframe(dense=True)
if X.shape[1] != Theta.shape[0]:
X = X.T
assert list(X.columns) == ids, "table and solution disagree on feature order"
tax = pd.read_csv("taxonomy-export/taxonomy.tsv", sep="\t", index_col=0)
degree = (P.abs() > 0).sum(axis=1) - 1
hub = degree.idxmax()
neighbours = P.loc[hub][P.loc[hub] != 0].drop(hub).sort_values()
print(tax.loc[hub, "Taxon"])
print(neighbours.to_frame("partial_r").join(tax["Taxon"]))
Note
solution/precision_ holds the sparse block \(\hat{\Theta}_S\). The low-rank part
is stored separately as solution/lowrank_. Reading edges off precision_
therefore gives you the sparse network conditional on the latent directions,
which is what you want here — but it is not the full precision matrix, so never
conflate the two in a figure caption.
Alongside the blocks the artifact carries an ordered labels/ group — one
entry per feature, in the order the covariance matrix was built in — taken from
the column names of the input correlation matrix. That group is the authoritative
ID list, which is why the snippet reads IDs from it and uses the exported table
only as a cross-check. If the assertion fires, do not silently reorder: a
mismatched order relabels the whole network.
Note
The taxonomy join needs matching key spaces, and the current bundle has them.
atacama-top-300-clr.qza is built with --p-keep-original-id, so its features —
and therefore the labels/ group of every solution derived from it — are the
same 32-character hexadecimal IDs that key
atacama-taxonomy-silva138.qza. tax.loc[hub, "Taxon"] works directly, and all
300 features resolve.
If you are working from an older copy whose features are ASV-1 … ASV-300, the
join will not raise — it returns all-NaN, which is easy to miss. Rebuild with
--p-keep-original-id as The 300-ASV Dataset shows, rather than
mapping the names back, because the mapping cannot be recovered.
Important
Do not recover the mapping from top-300-asvs.tsv by abundance rank. It is
tempting — the file has feature-id / total-abundance / abundance-rank, the
relabelling helper sorts ascending by total abundance, so ASV-1 is the least
abundant and the mapping looks like
\(\texttt{ASV-}n \longleftrightarrow \texttt{abundance-rank} = 301 - n\).
The mapping fails, and it fails silently. Total abundance is not unique: 209 of
these 300 features share a total-abundance value with another feature (61 tie
groups, the largest holding 13). Within a tie group the rank order is arbitrary,
so abundance-rank and the plugin’s internal ordering are free to disagree — and
they do. Permuting the correlation matrix by this mapping fails to reproduce the
shipped one, off by 1.137. Only the 91 features with a unique total abundance are
placed correctly. The rest get a neighbour’s taxonomy, and nothing raises.
The drift is not hypothetical. The helper originally ordered features with
df.sort_index(), whose default quicksort is not stable, and a later change to a
stable sort moved 158 of the 300 features to different ASV-k labels. Every
published number was unaffected — the graphical-lasso objective is invariant
under permutation, so the λ path, the eBIC at every grid point and the 216 edges
are bit-identical — but every feature identity shifted.
With --p-no-keep-original-id, ASV-k is a position, not an identifier. It is
only meaningful within the single artifact that defines it, and it is never a
key you can join on across artifacts.
Note
The taxonomy file’s columns are Feature ID, Taxon and Consensus — if you
ever need the classifier’s confidence, the column is Consensus, not
Confidence.
Four rules for reading the result.
Scale. Work in partial correlations, not raw precision entries. Precision entries depend on the scaling of the covariance and are not comparable between rows; partial correlations are bounded in \([-1, 1]\) and comparable across the matrix.
Sign. A negative partial correlation is not competition and a positive one is not cooperation. It is a conditional association among log-ratio-transformed abundances, and the compositional geometry alone can produce either sign [C5, C6]. The honest reading is “these two vary together after conditioning on everything else in the table”, full stop.
Degree. A high-degree node is a candidate hub, but degree is a function of \(\lambda_1\) before it is a function of biology: at a smaller penalty every degree goes up. Compare degrees within one fitted network. Do not quote a degree as a property of the organism.
The sub-composition. Every edge is conditional on the top-300 table. Removing or adding features changes the CLR reference and therefore the whole geometry, so the warning in The 300-ASV Dataset applies to every statement on this page. “Taxon A and taxon B are conditionally associated” always carries the silent suffix “within this sub-composition, at this \(\lambda_1\) and this \(\gamma\)”.
Edges the latent block removes#
The most informative comparison for the Atacama data costs nothing extra: the edges present in the SGL network but absent in the SLR network at the same \(\lambda_1\). Those are the associations that a small number of global directions can explain — candidate environment-mediated or batch-mediated edges. The edges present in both are the ones that survive conditioning on the latent subspace.
On this dataset the difference is small and highly structured. Adding two latent dimensions removes 14 of the 216 edges and adds none, and the removed edges fall into three components — one of 6 nodes carrying 9 of them, one of 4 nodes carrying 4, and a single isolated pair. The remaining 202 edges are common to both models.
The removed edges were the weak ones. Their partial correlations have median \(|r| = 0.021\) against \(0.116\) for the edges that survive — and the strongest edge the latent block removed, \(|r| = 0.062\), is weaker than the median surviving edge. The latent component does not compete with the strong structure in the network. It removes edges from the bottom of the edge-weight distribution.
They are clustered, not spread. Fourteen edges over 12 nodes, nine of them inside a single 6-node group that was nearly complete before the latent block was added. That is the shape you expect when one unmeasured driver acts on a handful of taxa at once and induces weak mutual correlation among all of them. A driver acting on the whole community would have thinned edges everywhere instead.
Neither observation proves the driver is environmental — see What you cannot conclude — but together they say the rank-2 block is doing something specific and local rather than shaving the network uniformly, which is what makes those 12 nodes worth looking up in the taxonomy.
Export the sparse-only solution alongside the one you already unpacked:
qiime tools export \
--input-path atacama-top-300-sgl-linear-path.qza \
--output-path sgl-export
import numpy as np
import zarr
def edges(path):
root = zarr.open(store=zarr.ZipStore(path, mode="r"))
T = np.asarray(root["solution/precision_"])
iu = np.triu_indices_from(T, k=1)
return set(zip(*(idx[T[iu] != 0] for idx in iu)))
sgl = edges("sgl-export/problem.zip")
slr = edges("slr-rank2-export/problem.zip")
print("SGL only :", len(sgl - slr))
print("both :", len(sgl & slr))
print("SLR only :", len(slr - sgl))
Check the third count. Edges can also appear when the latent block is added, because removing a dense confounding direction can unmask a direct association that was previously cancelled out. An SLR network that is a strict subset of the SGL network is a possible outcome, not a guaranteed one. Inspect an “SLR only” set of any size edge by edge.
Note
The three counts, and the taxonomy of the edges in each set, are pending recompute. The comparison figures that circulated with the superseded \(\lambda = 0.95\) bundle described in the overview cannot be used for this — they were computed at a different penalty.
The network and the log-contrast coefficients#
Log-Contrast Models at Scale fits, for each environmental outcome \(t\), a sparse coefficient vector \(\hat{\beta}^{(t)}\) subject to \(\mathbf{1}^\top \beta = 0\) — a different estimator on the same matrix. The two do not estimate the same quantity:
An edge is a conditional dependence between two taxa. It involves no outcome variable at all.
A log-contrast coefficient is a weight in a zero-sum contrast predicting an outcome. A taxon selected by the regression is selected relative to the other taxa in the contrast [C2, C3, C7]; the coefficient of a single feature has no meaning in isolation.
Three comparisons follow from that.
1. Are the selected taxa neighbours? Take the features with nonzero \(\hat{\beta}^{(t)}\) for one outcome and look them up in the partial-correlation matrix. If they form a connected subgraph, the regression is picking up a coherent module and the two methods are describing one structure from two directions. If they are scattered isolated nodes, the regression is exploiting marginal signal that the network sees as conditionally independent — which is possible and not necessarily wrong, but it means the two results are separate findings rather than mutual corroboration.
2. Does the latent subspace explain what is predictable? This is the comparison the appendix formalises, and it is the external check the rank-2 choice rests on. For each task \(t\) it defines
\(m_t\), the strongest correlation between any robust principal component and the outcome — computed with the procedure in Latent Components & Covariates; and
\(q_t = \lVert U^\top \hat{\beta}^{(t)} \rVert_2^2 / \lVert \hat{\beta}^{(t)} \rVert_2^2\), the fraction of the coefficient vector lying in the latent subspace \(U\).
If \(m_t\) and \(q_t\) are rank-correlated across tasks, then the outcomes the regression can predict are exactly the outcomes aligned with the latent subspace, and two dimensions are carrying the predictable structure. That is a statement about the rank, which is why it also belongs to the rank argument in Choosing the Latent Rank.
Important
The Spearman correlation between \(m_t\) and \(q_t\) that appears in the appendix and in the earlier drafts of the Atacama chapters is pending recompute. Do not quote it until it has been recomputed at \(\lambda_1 = 0.8\), \(\gamma = 0.3\), \(\mu_1 = 15\). It is also a correlation over a small number of tasks with strongly inter-correlated outcomes: report it with the number of tasks and a permutation \(p\)-value, and do not treat it as an independent confirmation of anything.
3. Watch for the same signal counted twice. If a latent component tracks elevation and a log-contrast model predicts elevation well, you have observed one gradient through two instruments, not two independent lines of evidence. The filtered-covariate analysis on the q2-classo page guards against the same failure: when a covariate is nearly a proxy for the outcome, adding it as a predictor inflates \(R^2\) without adding microbial information. Apply the same scepticism to a network-to-regression agreement.
What you cannot conclude#
Important
Nothing here is causal. Conditional dependence is not interaction. A coefficient is not an effect. Every result for the Atacama data is observational, from a single sampling campaign, with \(n = 54\).
Nothing here is about absolute abundance. The model lives in log-ratio coordinates. An edge or a coefficient describes relative structure within the top-300 sub-composition and can move when the composition changes for reasons that have nothing to do with the taxa involved.
Nothing here is penalty-free. The edge set is a function of \(\lambda_1\) and \(\gamma\); the rank is a function of \(\mu_1\); the selected features are a function of the cross-validation fold assignment and the one-standard-error rule. Report all of them, or the result is not reproducible even in principle.
The sample size limits what is checkable. With 54 samples, 300 features and strongly inter-correlated covariates, individual edges are not stably estimated. Read modules and gradients, not single links. If a specific edge carries the weight of an argument, check its stability: refit on subsamples and record the selection frequency [M4].
Note
Earlier drafts of the Atacama chapters named a Pseudarthrobacter ASV as the leading selected feature across the log-contrast models, a genus well described in hyperarid Atacama soils [D1, D4, D5]. That reading is plausible, but the identity of the leading feature is an output of a model that has not been re-run, and it is pending verification against QIIME 2 2026.7.
Reporting checklist#
Carry this from Tier 2 to your own data. A network result is reportable when it comes with:
the table it was estimated on — feature count, sample count, and the filtering rule that produced them;
the transform (
clrormclr) and the pseudo-count, if any;--p-methodfor the covariance and whether it was scaled;\(\lambda_1\), \(\gamma\), and how \(\lambda_1\) was selected;
for an SLR model, \(\mu_1\) and the achieved rank, since the rank is an output;
what the latent components correlated with, including the sequencing-depth check;
the software versions of both the plugin and GGLasso itself.
Points 5 and 7 go missing most often, and without them a sparse + low-rank result cannot be reproduced.
Further reading#
The method-level comparison of SGL, SLR and the adaptive model is in the Tier 1 chapter Network Interpretation and Analysis. The definitions behind \(m_t\), \(q_t\) and the eBIC are in Appendix: Mathematical Background. The works cited here are collected on the References page.