How CCCPM Works
This page explains the method behind CCCPM so you can configure an analysis with intent and interpret it correctly. For a runnable walkthrough, see the regression and classification examples; for the outputs, see Interpreting Results.
The CPM idea in one paragraph
Connectome-based predictive modeling (CPM) builds an interpretable model that links brain connectivity to an outcome. For each connection (edge) it measures how strongly that edge relates to the target, keeps the most related edges, summarizes them into a small number of network strength scores, and fits a simple model from those scores to the target. Because the final model is low-dimensional and the selected edges are inspectable, CPM is both predictive and interpretable.
The pipeline
CCCPM runs the following inside a cross-validation loop so that performance is always estimated on held-out data.
1. Edge selection
Each edge is correlated with the target, and edges below a p-value threshold are kept and split into a positive network (edges that increase with the target) and a negative network (edges that decrease with it). Available statistics:
| Choice | Values | What it decides |
|---|---|---|
selection_statistic |
"pearson", "spearman" (default) |
which correlation measures edge–target association |
selection_input |
"raw" (default), "residualized" |
whether selection controls for the covariates |
A binary target needs no special statistic: point-biserial correlation is
Pearson against a 0/1 outcome, which is exactly what "pearson" computes.
selection_input="residualized" fits one regression per edge, y ~ 1 + Z + edge.
It reports the semipartial correlation as the effect size and the edge
coefficient's p-value, with df = N − 2 − C for C covariates. This is a
deliberate design decision reported in your methods section, not something the
inner CV tunes per fold — it never enters the parameter grid.
UnivariateEdgeSelection(
selection_statistic="pearson",
selection_input="residualized", # selection controls for covariates
edge_selection=[PThreshold(threshold=[0.05], correction=[None])],
)
Why a coefficient test rather than correlating residualized edges
Correlating residualized edges against the target does not hold its nominal
alpha: the more confounded the data, the more the effective threshold
shrinks, so threshold=0.05 stops meaning 0.05. The per-edge coefficient
test keeps the threshold nominal whatever the confounding.
Dropping structural zeros (sparse connectomes)
For sparse structural connectomes (e.g. DTI streamline counts), many edges are zero for most subjects. The optional presence filter removes them before selection:
UnivariateEdgeSelection(selection_statistic="pearson", presence_filter=0.5)
presence_filter keeps an edge only if it is nonzero in at least the given fraction of
subjects (True = 0.5, the majority; a float sets the fraction). It is computed per
fold on the training subjects from the connectome alone, so it adds no target leakage,
and it is additive to the built-in near-zero-variance gate. It is not a test of
the mean against zero — an edge present in a small but consistent minority is dropped,
by design. Leave it off (False, the default) for functional data, whose edges have a
real signed distribution around a mean of ~0. The filter always sees the raw
connectome: model_input="residualized" is applied after selection, so the two
choices stay independent.
Keeping only connected edges
UnivariateEdgeSelection(selection_statistic="pearson", connected_components=True)
connected_components keeps only edges that belong to a connected component (per
positive/negative network) with at least a minimum number of edges, dropping isolated
single edges. True uses a minimum of 2 edges (drop lone edges); an int sets the
minimum explicitly. Applied per fold (and per permutation) after thresholding, this
favours coherent subnetworks over scattered one-off edges and can improve edge
stability.
2. Confound control
Confound control is two independent run-level choices, not a menu of model variants:
| Choice | Where | Question it answers |
|---|---|---|
selection_input |
UnivariateEdgeSelection |
does edge selection control for the covariates? |
model_input |
CPMAnalysis |
is the covariate variance regressed out of the connectome the models consume? |
model_input="residualized" fits the residualization on the training fold and
applies it to the test fold, after edge selection — so the two axes never
interfere. Together they form a 2×2, and each cell is its own run:
CPMAnalysis(
results_directory="results/controlled",
edge_selection=UnivariateEdgeSelection(
selection_statistic="pearson",
selection_input="residualized", # axis 1
edge_selection=[PThreshold(threshold=[0.05], correction=[None])]),
model_input="residualized", # axis 2
)
Why deconfounding is a run-level choice, not a model called residuals
OLS is invariant to it once the covariates are in the design — residualizing
moves variance from the strength column into the Z columns, which are
already there — so full and increment do not move. The non-linear
backends are not invariant: on identical edges, full shifts by 391% of
sd(y) for DecisionTreeCPM, 65% for RandomForestCPM and 19% for GAMCPM.
A model name would therefore mean something different for every backend,
and you could not tell which connectome produced full. Making it a
property of the run removes the ambiguity.
Covariates are always available to the model variants below, so you can quantify what the connectome adds beyond them.
Running without covariates
covariates is optional. Passing none is vanilla CPM: only connectome is
defined, and covariates / full / increment are NaN-filled — the results
keep their full shape, and available_models.json tells the report which rows
carry a real number. Options that presuppose covariates
(selection_input="residualized", model_input="residualized") raise up front,
naming the offending parameter, rather than silently degrading into a different
analysis.
3. Model variants
For each fold, CCCPM fits several models so you can separate the connectome's contribution from the covariates':
connectome— network strengths only.covariates— covariates only (a baseline).full— connectome + covariates.increment— the added value of the connectome over covariates (full−covariates).
Each is evaluated on the positive, negative, and combined networks. Whether the
connectome behind connectome and full has been deconfounded is a property of
the run (model_input), not a separate model name.
increment is reported as NaN for Pearson r and F1, and this is
deliberate: a difference of two correlations is not itself a correlation (that
needs Fisher z / Steiger's test), and a difference of two F1 scores is not an
F1. It is available for explained variance, MSE, MAE, accuracy, balanced
accuracy and ROC AUC.
4. Nested cross-validation (optional)
An outer CV loop estimates unbiased performance. An optional inner CV loop
tunes hyperparameters (most importantly the p-threshold) without leaking test data —
pass an inner_cv and multiple thresholds to enable it.
5. Edge stability
When edges are selected repeatedly across folds, CCCPM records how often each edge is
chosen — its stability. Edges selected in most folds are the reliable ones, and
select_stable_edges=True restricts the model to edges above stability_threshold.
6. Permutation testing
With n_permutations > 0, the whole pipeline is re-run on many shuffled copies of the
target to build a null distribution of performance. The resulting p-value is the
fraction of permutations that match or beat the real result — your evidence that the
brain–behaviour association is not due to chance. Use 1000+ permutations for
publishable significance.
7. Edge-stability significance
The same permutations also give a null distribution of edge stability, so CCCPM can
test whether an edge is selected across folds more consistently than chance. Because
there are tens of thousands of edges and permutation p-values are floored at
1/(n_perm+1), per-edge FDR is hopeless and a per-edge max-statistic is crippled by the
discreteness of stability. CCCPM instead controls the family-wise error rate at the
subnetwork level, selected via stability_significance_method:
"nbs"(default) — the Network-Based Statistic (Zalesky et al., 2010). Edges abovenbs_stability_thresholdform a graph; its connected components are tested against a permutation null of the largest component (size =nbs_component_stat="extent", or summed supra-threshold stability ="intensity"). A significant result means this connected subnetwork is selected more consistently than chance — not a claim about any single edge."tfce"— network Threshold-Free Cluster Enhancement, giving per-edge FWER-corrected p-values with no arbitrary cluster-forming threshold.
Both write stability_edges_significance.npy (per-edge p-values; NBS edges carry their
subnetwork's p-value) and a stability_edges_significance_meta.json with the null
distribution and component diagnostics that the HTML report visualises.
Putting it together
A typical confound-aware analysis:
- Set
selection_input="residualized"so covariates cannot drive which edges are selected. - Set
model_input="residualized"so they cannot drive the model either. (For the linear backend this leavesfullandincrementunchanged — it is theconnectomemodel it protects.) - Use an inner CV to tune the p-threshold.
- Enable permutation testing.
- Read the
incrementmodel's significance to claim the connectome adds information beyond your covariates — on a metric whereincrementis defined (explained variance, not Pearson r).
See Interpreting Results for how to read every output.