Regression Example
This walkthrough runs a complete connectome-based predictive modeling (CPM)
analysis for a continuous target (regression) on simulated data. The script
below is the runnable file examples/regression_quickstart.py — copy it and swap in
your own data.
The full script
"""
Quickstart: Connectome-based Predictive Modeling for a *regression* target.
This script runs a complete CPM analysis on simulated data in a few lines:
it selects predictive edges, fits the models inside a cross-validation loop,
assesses significance with permutation testing, and writes an HTML report.
Run it with:
poetry run python examples/regression_quickstart.py
The companion script ``classification_quickstart.py`` shows the same workflow
for a binary target.
"""
from sklearn.model_selection import KFold, ShuffleSplit
from cccpm import CPMAnalysis, UnivariateEdgeSelection, PThreshold
from cccpm.simulation.simulate_sem import simulate_data_given_kappa
# ---------------------------------------------------------------------------
# 1. Get some data
# ---------------------------------------------------------------------------
# In a real study these would be your own arrays:
# X : connectome edges, shape (n_samples, n_features)
# y : the (continuous) outcome you want to predict, shape (n_samples,)
# covariates : nuisance variables to control for (e.g. age, motion, sex),
# shape (n_samples, n_covariates)
#
# Here we use the SEM-based simulator, which builds data with a *known* ground
# truth: a latent confound Z is a common cause of both the connectome and the
# outcome. ``kappa`` is the fraction of the naive brain–outcome R² that is
# actually confound-driven, so ``kappa=0.3`` means 30% of the apparent signal is
# spurious and confound control genuinely matters.
sim = simulate_data_given_kappa(
R2_X_y=0.4, # naive R²(y ~ X): apparent brain–outcome strength
kappa=0.3, # 30% of that R² is driven by the confound
n_features=4950, # a 100-node connectome (100*99/2 = 4950 edges),
# matching the built-in Schaefer100 atlas used below
n_features_informative=40, # "mixed" edges: real signal + confound leakage
n_pure_signal_features=20, # edges tied to y but NOT the confound
n_confound_only_features=20, # edges tied to y ONLY through the confound
n_confounds=2,
n_samples=200,
random_state=42,
)
X, y, covariates = sim["X"], sim["y"], sim["Z"]
# The simulator records the analytic ground truth, so we know what "good"
# deconfounded performance should look like before we even run the model.
info = sim["info"]
print(f"Naive R²(y~X) : {info['R2_X_y']:.2f} (inflated by the confound)")
print(f"True R²(y~X | Z) : {info['R2_X_y_given_Z']:.2f} (what we hope to recover)")
# ---------------------------------------------------------------------------
# 2. Configure edge selection
# ---------------------------------------------------------------------------
# Pick which edges enter the model by correlating each edge with the target and
# keeping those below a p-value threshold.
#
# Confound control is two independent choices. This one, `selection_input`,
# decides whether the *selection* controls for the covariates: 'residualized'
# tests each edge with the regression y ~ 1 + Z + edge instead of a plain
# correlation. The other, `model_input`, is set on CPMAnalysis below.
#
# Passing several thresholds turns the p-threshold into a tuned hyperparameter;
# the inner CV below picks one per outer fold, so the choice never sees the
# outer test set.
edge_selection = UnivariateEdgeSelection(
selection_statistic="pearson",
selection_input="residualized", # covariates controlled during selection
edge_selection=[PThreshold(threshold=[0.05, 0.01, 0.001], correction=[None])],
)
# ---------------------------------------------------------------------------
# 3. Configure and run the analysis
# ---------------------------------------------------------------------------
cpm = CPMAnalysis(
results_directory="./results/regression_quickstart",
task_type="regression", # or leave as None to auto-detect
cv=KFold(n_splits=10, shuffle=True, random_state=42),
inner_cv=ShuffleSplit(n_splits=1, test_size=0.2, random_state=42),
# tunes the p-threshold inside each
# outer fold (nested CV)
edge_selection=edge_selection,
model_input="residualized", # the second confound-control axis:
# deconfound the connectome the models
# consume, after edge selection
n_permutations=1000, # use 1000+ for a real analysis
atlas="Schaefer100-17", # built-in atlas → brain plots in the
# report; or pass a path to a custom
# CSV (region,x,y,z[,network]).
# See cccpm.atlases.list_atlases().
device="cpu", # "cuda" uses the GPU if available
stability_significance_method="nbs", # how *stability* significance is
# tested -- unrelated to the
# PThreshold above
nbs_stability_threshold=0.5, # a fraction of folds, not a p-value
)
cpm.run(X=X, y=y, covariates=covariates)
# ---------------------------------------------------------------------------
# 4. Inspect the results
# ---------------------------------------------------------------------------
# Everything is written under the results_directory, including:
# - cv_results_summary.csv : performance metrics (Pearson r, MAE, MSE, ...)
# - p_values.csv : permutation-based significance
# - cv_predictions.csv : out-of-sample predictions per subject
# - report.html : a full, human-readable HTML report
#
# This run has confound control on both axes. To see the confound inflation the
# SEM simulator built in on purpose, run it again with selection_input='raw' and
# model_input='raw' and compare the 'connectome' model across the two: the gap is
# the inflation. Within a single run, 'increment' (full - covariates) answers the
# same question -- read it on explained variance, not Pearson r, where a
# difference of correlations is not a statistic and the cell is NaN by design.
print("Done. Open ./results/regression_quickstart/report.html to explore the results.")
Step by step
1. Data. CCCPM expects three aligned arrays:
X— the connectome, shape(n_samples, n_features), wheren_featuresare the upper-triangular edges of a symmetric node-by-node matrix (n_features = n_nodes * (n_nodes - 1) / 2). You can also pass full matrices of shape(n_samples, n_nodes, n_nodes)and CCCPM will vectorize them.y— the continuous outcome, shape(n_samples,).covariates— nuisance variables to control for (e.g. age, sex, motion).
The script fills these arrays with the SEM-based simulator, which bakes in a known amount of confounding so confound control actually matters — see Simulating Data for how it works and how to tune it.
2. Edge selection. UnivariateEdgeSelection correlates each edge with the target
and keeps edges below a p-value threshold. Pick the correlation with
selection_statistic= ("pearson" or "spearman"), and control for covariates
during selection with selection_input="residualized", which tests each edge
by the regression y ~ 1 + Z + edge instead.
2b. Confound control has a second, independent switch. selection_input
decides how edges are chosen; model_input on CPMAnalysis decides whether
the connectome the models consume has been deconfounded. Set both to
"residualized" for the leak-free configuration. See
Confound control for why this is two run-level
choices rather than an extra model.
3. Run. CPMAnalysis performs the cross-validated fit, and — because
n_permutations > 0 — permutation testing for significance. For a real analysis,
prefer 1000+ permutations.
4. Results. Everything is written under results_directory, including
report.html. See Interpreting Results for a full guide
to the metrics, model variants, and output files.
See also
- Classification example — the same workflow for a binary target.
- Interpreting Results — what every output means.