Migrating from v0.5.1¶
nltools 0.6.0 delegates to nilearn, scikit-learn and polars instead of
re-implementing them. The data classes keep their jobs, but their names, keyword
vocabulary and return types line up with each other and with the libraries
underneath: Brain_Data is BrainData, nltools.stats is nltools.algorithms,
prediction and regression return typed records instead of loose dictionaries, and
one keyword name means one thing everywhere. There are no compatibility aliases —
a v0.5.1 name either has a listed replacement or is gone.
BrainCollection and collection-level execution are deferred to 0.6.1; in 0.6.0,
apply BrainData methods per subject and stack the results with
concatenate.
Quick reference¶
| Area | v0.5.1 | v0.6.0 | Note |
|---|---|---|---|
| Data classes | Brain_Data, Design_Matrix |
BrainData, DesignMatrix |
PEP 8 names; no aliases |
| Data classes | brain.shape(), brain.isempty() |
brain.shape, brain.is_empty |
Properties, not methods |
| Data classes | brain.empty() |
brain.create_empty() |
empty was ambiguous with the predicate |
| Data classes | brain.nifti_masker |
nilearn.masking.apply_mask(img, brain.mask) |
Removed |
| Data classes | brain.apply_mask(mask, resample_mask_to_brain=True) |
brain.apply_mask(mask) |
The mask must already sit on the data's grid; resample() first |
| Data classes | brain.threshold(2, -2), brain.filter(0.5, 0.008) |
brain.threshold(upper=2, lower=-2), brain.filter(sampling_freq=0.5, high_pass=0.008) |
Options are keyword-only after the data argument |
| Data classes | brain.decompose(algorithm='ica') |
brain.decompose(method='ica') |
method names the algorithm; metric is reserved for distances |
| Data classes | brain.append(data, **kwargs) |
brain.append(data, *, ignore_attrs=False) |
Extra keywords are no longer forwarded to pandas |
| Data classes | adjacency.shape() (vector), square_shape() |
adjacency.shape (square), adjacency.vector_shape |
The logical shape is the default |
| Data classes | Groupby, brain.groupby(), brain.aggregate() |
Removed, no successor | Loop over extract_roi output |
| Data classes | brain.icc() |
Removed, no successor | Use pingouin.intraclass_corr |
| Data classes | nltools.prefs.MNI_Template |
set_brainspace(), get_brainspace(), reset_brainspace(), with_brainspace() |
Functions, not a mutable singleton |
| Data classes | Legacy (≤ 0.5.1) HDF5 files load | BrainData(path) raises ValueError |
Export to NIfTI or CSV under 0.5.1 before upgrading |
| Data classes | Resampling to a resolution kept only the positive-coordinate octant — 8,039 of 238,955 voxels on the 2 mm MNI mask | brain.resample(resolution=3) keeps all 70,831 voxels |
The target grid encloses the source field of view instead of starting at world zero |
| Data classes | len(single_image) was its voxel count and single_image[0] raised |
len() is 1, and a single image is indexable and iterable |
Inherited v0.5.1 behaviour |
| Data classes | A one-voxel mask squeezed three images into one three-voxel image | The voxel axis is kept; only a leading singleton image axis collapses | Affects objects whose mask holds a single voxel |
| Data classes | brain[0] = other matched metadata columns by position and turned a mixed Int64/String frame into Object |
Columns are matched by name and keep their dtypes; a name mismatch raises | Inherited v0.5.1 behaviour; the check is the one append already applied |
| Data classes | X or Y with a row count that did not match the data was accepted |
Construction and assignment raise ValueError |
The mismatch used to surface later, from an unrelated operation |
| Data classes | BrainData(other, mask=submask) reinterpreted the packed columns under the new mask |
The values are re-extracted onto the new support | Voxels keep their locations; a wider mask widens with zeros |
| Data classes | BrainData([bd1, bd2]) dropped .X/.Y and ignored mask= |
Row metadata is concatenated and mask= is applied |
Same result as concatenate followed by the mask |
| Data classes | BrainData(path.h5, mask=other) reinterpreted the stored columns under the new mask |
They are re-extracted onto that mask, and a stored fit is dropped when the voxel axis changes | The warning now says the stored mask describes the stored columns |
| Data classes | a + b, a.append(b) and a[0] = b[0] combined objects whose voxels were different voxels |
Each raises ValueError unless the two grids and supports match |
Inherited v0.5.1 laxity; use resample() and apply_mask() to establish a common voxel axis |
| Data classes | brain.threshold(lower=3, cluster_threshold=50) returned the positive tail, and upper=3 kept the negative one too |
The clustered result keeps the same tail as the unclustered one | nilearn's two_sided keys off the cutoff's sign, not the tail you asked for |
| Data classes | brain.standardize() cast the result back to integer or boolean input dtype |
Integer and boolean data comes back floating | A centred int32 column of [0, 1] was [0, 0]; float32 input is unchanged |
| Data classes | brain.filter(runs=…, sample_mask=…) gave every run's rows the first run's X and Y |
Row metadata follows the rows nilearn returns | clean cleans one run at a time in np.unique(runs) order |
| Design matrix | pandas subclass | polars-backed class | .to_numpy(), .with_columns(), .corr() |
| Design matrix | Design_Matrix_Series |
Removed, no successor | A single column is a polars.Series |
| Design matrix | dm.polys |
dm.confounds |
Read-only; set columns as confounds when you append them |
| Design matrix | dm.zscore(columns=…) |
dm.standardize(method='zscore', columns=…) |
Keyword-only; the default method='center' centers only |
| Design matrix | dm.convolve(conv_func='hrf') |
dm.convolve(kernel='glover') |
Six nilearn HRF names or an array; 'hrf' is gone |
| Design matrix | dm.convolved listed the source columns |
dm.convolved lists the <col>_c{i} outputs |
The suffix and the dropped source column are unchanged |
| Design matrix | Convolved values from a nipy-derived kernel | nilearn's compute_regressor at oversampling=50 |
Every beta, t and contrast moves (#492) |
| Design matrix | poly_0, cosine_1, global_spike1 |
.nl_poly_0, .nl_cosine_1, .nl_global_spike1 |
Generated columns live in the reserved .nl_ namespace |
| Design matrix | dm.vif(exclude_polys=…), dm.clean(exclude_polys=…, verbose=…) |
dm.vif(exclude_confounds=…), dm.clean(exclude_confounds=…, progress_bar=…) |
Renamed with .polys |
| Design matrix | dm.append(dm=…) |
dm.append(data=…) |
data is the second operand on all three data classes |
| Design matrix | dm.heatmap() |
dm.plot() |
One plotting method name per class |
| Design matrix | find_spikes emitted duplicate regressors |
One regressor per spike | The design is full rank again |
| Design matrix | dm.convolve() silently replaced an existing <col>_c0 column |
convolve raises ValueError naming the column |
Rename or drop the column before convolving |
| Design matrix | find_spikes put a diff_spike indicator on the volume before the frame-to-frame jump |
The indicator marks the volume the jump moved into | The framewise-displacement convention; regenerated confound regressors change, and a spike that used to collide with a global detection may now add a column |
| Design matrix | find_spikes raised on a 4D NIfTI with a singleton spatial axis (a single-slice acquisition) |
It returns the spike design | Only trailing singleton axes are squeezed, so (x, y, z, t, 1) input still works |
| GLM | brain.regress(mode='ols') → dict |
brain.fit(model='glm', X=dm), read brain.model, then compute_contrasts |
The fit is a frozen FitResult with betas, predicted, residual and r2; t/p are per contrast, not per regressor |
| GLM | nltools.stats.regress(X, Y, mode=…) |
nltools.algorithms.regress(X, Y, *, stats=…, tail=…) |
OLS was the only working mode; robust/ARMA are gone |
| GLM | brain.randomise(...) |
brain.ttest(permutation=True) |
Voxelwise permutation on the entry point that already existed |
| GLM | adjacency.regress(X, mode='ols') |
adjacency.regress(X) |
Same removal as the standalone regress; tail is keyword-only |
| GLM | Before this fix, a ridge FitResult scored a non-finite voxel 0.0 |
That voxel's r2 is NaN |
v0.5.1 had no ridge FitResult; a finite constant target still scores exactly 0.0 |
| Prediction | brain.predict(algorithm='svm', cv_dict=…) → dict |
brain.predict(y=…, estimator=…, cv=…) → PredictResult |
Frozen record with weight_map, scores, predictions |
| Prediction | brain.predict_multi(...) |
brain.predict(spatial_scale='roi'\|'searchlight') |
One entry point, three spatial scales |
| Prediction | set_cv(Y, cv_dict) |
cv=<int> or an sklearn splitter, plus groups= |
Removed; an int is that many unshuffled stratified folds |
| Prediction | algorithm='svm', 'logistic', 'svr' |
estimator='linear_svc', 'logistic_regression', 'linear_svr' |
Abbreviations are rejected by name; linear, lassopcr and the *CV variants are gone |
| Prediction | algorithm='ridge' fitted Ridge() at its default penalty |
estimator='ridge' fits RidgeCV over a 1e-3…1e6 grid |
Pass estimator_kwargs={'alphas': …} for a fixed penalty |
| Prediction | Roc(threshold_type='optimal_overall') |
Roc(method='optimal_overall') |
Keyword-only, and calculate takes method= too |
| Prediction | Roc reported metrics from non-finite decision values |
Roc raises ValueError |
One NaN score made every criterion NaN, so nothing counted as misclassified |
| Prediction | An (n, 1) score column reported accuracy 0.5 on perfectly separated data |
The column is read as one value per observation | misclass came back as an (n, n) matrix |
| Prediction | Integer forced-choice scores reported accuracy 0.0 for a perfect subject | Scores are coerced to float | Pair centering was truncated back into the integer array |
| Prediction | AUC came from a 50-points-per-observation grid: tied scores gave 0.0, perfectly ordered data 0.75 | AUC comes from the data's own operating points, matching sklearn.metrics.roc_curve |
class_thr and criterion_values move with it; criterion_values now ends at np.inf, the corner class_thr is never chosen from |
| Prediction | Forced-choice calculate() gave one answer on the first call and another from the second on |
Every call gives the same answer | Pair centering is derived per call instead of overwriting input_values |
| Prediction | Forced-choice accuracy depended on the row order of the two classes | Errors are paired by subject id | A reordering of the same observations gave 0.0 where 0.5 is correct |
| Prediction | An unknown scoring name returned an all-NaN score_map for searchlight and ROI decoding |
Raises, naming the scorer | Whole-brain decoding already raised on the identical call |
| Similarity | brain.similarity(image=…, method=…) |
brain.similarity(data, *, metric=…) |
metric is the similarity metric everywhere |
| Similarity | adjacency.similarity(perm_type=…, ignore_diagonal=…) |
adjacency.similarity(data, *, method=…, include_diag=…) |
Polarity is flipped, so the default now excludes the diagonal |
| Similarity | adjacency.cluster_summary(metric=…, summary=…) |
adjacency.cluster_summary(summary=…, scope=…) |
summary is the central tendency; scope is within/between |
| Similarity | brain.extract_roi(metric=…) |
brain.extract_roi(method=…) |
metric is reserved for distances |
| Similarity | brain.multivariate_similarity(images, method='ols') |
brain.multivariate_similarity(images, tail=2) |
OLS was the only mode |
| Similarity | Manual per-ROI loop to paint an RSA map | brain.distance(spatial_scale='roi', roi_mask=atlas) then roi_to_brain_from_atlas |
Explicit atlas mapping |
| Similarity | brain.similarity(other) compared array position against array position whenever the two masks kept the same number of voxels |
Masks that differ are intersected first | Equal support size is not equal support |
| Similarity | brain.extract_roi(atlas) raised when the atlas's labels filled its mask |
The atlas is classified by its nonzero labels | A {1, 2} atlas with no background is an atlas, not a binary mask |
| Alignment | from nltools.external import SRM, DetSRM |
brain.align(method='probabilistic_srm'\|'deterministic_srm') |
The estimators are internal |
| Alignment | Procrustes back-projection was transformed @ T |
transformed @ T.T |
transformation_matrix is stored as transformed = original @ T |
| Alignment | Procrustes aligned Brain_Data subjects with different voxel counts by zero-padding the feature axis |
align(method='procrustes') raises; pass the .data arrays to get the padded result |
The padded result has no mask that can describe it |
| Alignment | An SRM n_features above a subject's voxel count silently fit a smaller model (deterministic_srm) or failed with a numpy broadcast error (probabilistic_srm) |
Both SRM estimators raise, naming the subject and both counts | The returned model was never the one that was asked for |
| Alignment | align_states(..., replace_zero_variance=True) on integer maps raised ValueError: matrix contains invalid numeric entries |
The maps are converted to float, so the replacement noise survives | The non-replacement path still keeps the caller's dtype |
| Statistics | nltools.stats |
nltools.algorithms |
Same functions, one namespace |
| Statistics | one_sample_permutation, two_sample_permutation, correlation_permutation, matrix_permutation |
Same names with a _test suffix |
Keyword-only after the data arguments |
| Statistics | adjacency.generate_permutations(n_perm=…) |
generate_permutations(n_permute=…) |
One spelling for a permutation count everywhere |
| Statistics | return_perms=True → out['perm_dist'] |
return_null=True → out['null_dist'] |
One name for the null distribution |
| Statistics | correlation_permutation, matrix_permutation defaulted to metric='spearman' |
Both default to metric='pearson' |
Pass metric='spearman' to reproduce v0.5.1 numbers |
| Statistics | correlation_permutation(method='circle_shift'\|'phase_randomize') |
Removed | Shift or randomize with circle_shift/phase_randomize before the test |
| Statistics | brain.ttest(threshold_dict={'fdr': .05}) |
brain.ttest(popmean=…, permutation=…) then nltools.algorithms.threshold |
Thresholding is its own step |
| Statistics | adjacency.ttest(permutation=…, **kwargs) |
Same keywords as BrainData.ttest |
Returns mean, t, z, p |
| Statistics | brain.bootstrap('mean', save_weights=…) → dict |
brain.bootstrap('mean', return_samples=…) → BootstrapResult |
estimate, standard_error, ci_lower, ci_upper |
| Statistics | summarize_bootstrap, regress_permutation |
Removed | The bootstrap entry points return the summary |
| Statistics | double_center, u_center |
Removed | Internal steps of distance_correlation |
| Statistics | adjacency.isc(), adjacency.isc_group() |
nltools.algorithms.isc(), isc_group() |
Standalone functions on arrays |
| Statistics | transform_pairwise alternated its sign flip over candidate pairs, leaving the classes unbalanced |
The +1 and -1 classes come out balanced |
Pairs the group and target filters skip no longer consume an alternation slot |
| Statistics | adjacency.bootstrap on a single matrix resampled its edges and returned a smaller matrix |
Raises ValueError |
It resamples matrices, so it needs at least two, like adjacency.ttest |
| Statistics | The social relations model took its grand mean before masking the diagonal | Every SRM average excludes the diagonal | Only a directed matrix whose diagonal holds self-ratings moves; a NaN diagonal is unchanged |
| Statistics | The social relations model paired each cell with the wrong reciprocal cell | Each dyad is paired with its own transpose | Variance components, reciprocity, reliabilities and the total variance all move |
| Statistics | The SRM summary printed two-sided p-values above one when t was negative |
2 * sf(abs(t), df) |
Printed covariance rows only; no returned value changes |
| Statistics | brain.bootstrap('mean') on a single image resampled its voxels as observations |
Raises ValueError |
The old result was one scalar wrapped in the source's full mask, which could not be written or plotted |
| Statistics | exclude_self_corr masked every resampled pair whose correlation reached 1 |
Only the pairs a resampled subject forms with itself are masked | Two distinct subjects that genuinely correlate at +1 or -1 now count, and identity is the only correct reading for metric='euclidean' or 'cosine' |
| Statistics | An ISC bootstrap counted undefined draws in the p-value denominator and let one of them make the whole interval NaN |
Undefined draws are dropped per feature: the denominator is that feature's valid-draw count and the interval is a NaN-aware percentile | The same data gave p = ⅙ as 2-D input and p = 1/21 once a singleton voxel axis was added; return_null=True now hands back every draw, NaN where a draw was undefined, rather than a shortened array |
| Statistics | A feature whose ISC was undefined reported p = 1 / (n + 1), the smallest p the correction can produce, with an interval taken over the resamples that survived |
p and both bounds are NaN for that feature |
Every flat or masked-out voxel used to surface as the most significant hit in a whole-brain map |
| Statistics | holm_bonf(np.array([0.03, 0.04])) returned 0.04 |
It returns -1 |
Holm is a step-down procedure, so the walk stops at the first p above its boundary; the new threshold is never larger than the old. fdr is a step-up procedure and is unchanged |
| Statistics | threshold(stat, p) resolved the two images spatially, through the NIfTI grid |
threshold and multi_threshold raise ValueError unless the two images cover the same voxels |
The masked-array rewrite applied the p-values by array position, so two images with disjoint masks of equal size thresholded the wrong voxels |
| Statistics | multi_threshold returned a cumulative map for a multi-image t_map; threshold returned an image-count vector when nothing survived |
Both keep the input's (n_images, n_voxels) shape |
0.6.0 raised a broadcast ValueError on the multi-image path (#304, #372) |
| Statistics | winsorize(x, cutoff={'quantile': [0, .75]}) on [nan, 0, 1, 2, 100] gave [nan, 0, 1, 2, 26.5] |
The same values | The polars rewrite returned all-NaN; quantile cutoffs skip missing values again, so winsorize(trim(x)) is meaningful |
| Statistics | winsorize([0, 1, 2, 3, 4], cutoff={'quantile': [.25, .75]}, replace_with_cutoff=False) returned all 2.0 |
It returns [1, 1, 2, 3, 3] |
A value sitting exactly on a cutoff is not an outlier, so it is its own closest existing value |
| Statistics | regress reported df = n - p, so X = ones((4, 2)), Y = 0..3 gave df=2, se=sqrt(5/32), p=0.198 |
df = n - rank(X): df=3, se=sqrt(5/48) |
A duplicated regressor no longer changes the inference; full-rank designs are bit-identical. Adjacency.regress reports the same df |
| Statistics | An integer design overflowed in X.T @ X: a column of 100000 gave t=0.710, p=0.529 where the same design in float gives t=3.873, p=0.030 |
X and Y are promoted to float first |
Only designs large enough to overflow move; 0/1 dummies are exact either way |
| Statistics | regress on an undefined fit (a NaN in Y) returned t=0, p=1 next to a NaN coefficient |
t and p are NaN |
A perfect fit, whose standard error is finite but near zero, still scores t=0, p=1 |
| Statistics | downsample(range(10), sampling_freq=5, target=2, target_type='hz') returned [0.5, 2.5, 4.5, 7.5] |
It returns [1, 3.5, 6, 8.5] |
Rows bin by floor(row / n_samples), the rule DesignMatrix.downsample already used; integer ratios are unchanged |
| Statistics | upsample dropped non-numeric columns with a UserWarning |
The same, naming the dropped columns | The polars rewrite raised ValueError: could not convert string to float; a frame with no numeric column now raises instead |
| Statistics | make_cosine_basis(nsamples, sampling_freq=…) |
make_cosine_basis(nsamples, sampling_interval=…) |
The value was always the sampling interval in seconds (SPM's RT); the name now says so. Positional calls are unchanged |
| Plotting | brain.plot(view=…, threshold_upper=…, axes=…) |
brain.plot(method=…, upper=…, ax=…) |
ax is the matplotlib spelling on every class |
| Plotting | adjacency.plot(limit, axes, *args) |
adjacency.plot(*, limit=3, ax=None) |
Keyword-only; no positional passthrough |
| Plotting | plot_brain, plot_t_brain, plot_interactive_brain |
Removed | BrainData.plot(method='glass'\|'mni'\|'full') and BrainData.iplot |
| Plotting | roc_plot, scatterplot, probability_plot, dist_from_hyperplane_plot |
Roc.plot(), BrainData.predict(plot=True) |
Drawn by the object that holds the results |
| Plotting | plot_mean_label_distance, plot_between_label_distance, plot_silhouette |
Adjacency.plot_label_distance and friends |
Methods on Adjacency |
| Plotting | plot_stacked_adjacency(a1, a2) |
a1.plot_stacked(a2) |
A method on Adjacency; each triangle keeps its own scale instead of being normalized onto a shared one |
| Plotting | brain.iplot(threshold=0, surface=…) |
brain.iplot(view=…, threshold=…, atlas=…) |
Rebuilt on niivue |
| Plotting | Glass brains hid a percentile of voxels (nilearn's threshold='auto') |
brain.plot(method='glass') draws every voxel |
The colorbar keeps its 0 tick; pass threshold= for a cutoff |
| Plotting | adjacency.plot() drew every matrix on a sequential ramp |
Matrices whose off-diagonal values cross zero use RdBu_r, centered at 0 with symmetric limits |
One-signed matrices are unchanged; cmap, center, vmin, vmax still win |
| Plotting | adjacency.squareform() always wrote a zero diagonal |
The diagonal follows matrix_type: 1 for a similarity, 0 for a distance |
Adjacency(sim.squareform()) now round-trips as a similarity |
| Plotting | adjacency.plot() on a stack with shared labels tick-labelled panel i with label i alone |
Every panel carries all the node labels | A nested per-matrix label grid is unchanged |
| Plotting | brain.plot(method='glass'\|'slices', ax=…) ignored ax and drew into a figure of its own |
ax reaches nilearn as axes= |
The caller's figure is returned and left open |
| Plotting | brain.predict(plot=True) drew the ROC and margin figures but not the weight map |
All three figures are drawn | The weight map was drawn and immediately closed |
| IO | onsets_to_dm(f, sampling_freq, run_length) |
DesignMatrix(events_path, run_length=…, TR=…) |
HRF-convolves by default; hrf_model=None for boxcars |
| IO | from nltools.external import glover_hrf |
from nilearn.glm.first_level import glover_hrf |
The five HRF wrappers were pass-throughs |
| Datasets | fetch_pain(data_dir=…, resume=…, verbose=1) |
fetch_pain(verbose=0) |
Caching is handled for you; same for fetch_emotion_ratings |
| Datasets | download_collection, get_collection_image_metadata |
fetch_neurovault_collection(collection_id) |
One function |
| Datasets | get_anatomical() |
nilearn.datasets.load_mni152_brain_mask() |
Removed |
| Simulator | create_data returned data on the template mask |
The data keeps the brain_mask the Simulator was built with |
Only affects a custom brain_mask; the default path is byte-identical |
| Simulator | create_ncov_data sized every covariance block by the row region |
Each block is sized by its own region | The matrix was asymmetric for regions of unequal size, so simulated values move |
| Simulator | create_ncov_data broadcast one noise image across every repetition |
Noise is drawn per repetition, as in create_cov_data |
sigma now varies within subject outside the regions |
| Simulator | SimulateGrid signal masks were a pixel wider than signal_width at mismatched parity, and the hit rates divided by signal_width**2 |
The mask is exactly signal_width wide and both rates divide by its size |
The true-positive rate could read 1.78; same-parity widths, including every default, are unchanged |
| Simulator | SimulateGrid's false discovery rate counted positive discoveries only, and was NaN with none |
Every non-zero discovery counts; an empty map scores 0.0 | multiple_fdr no longer carries NaN into its mean |
| Simulator | SimulateGrid.add_signal left the previous fit in place and fit() then raised |
add_signal clears the fit, so fit() runs again |
Statistics no longer describe data the object has replaced |
| Simulator | plot_grid_simulation redrew a cached thresholded map |
It re-thresholds with the arguments it was given | The middle panel and its caption describe the same threshold |
| Simulator | corrected_threshold recorded fdr(p) at the default q of 0.05 |
It records the cutoff at the requested q | The recorded value used to be -1 for maps with surviving pixels |
| Masks | collapse_mask returned an all-zero template-length vector for custom-space data and kept overlaps shared by fewer than all masks |
It collapses on the input's own voxel axis and drops every overlapping voxel | to_nifti() on a custom-space result used to raise; 2-mask and disjoint inputs are unchanged |
| Masks | expand_mask rounded the caller's mask to int32 in place |
The input is left untouched | The output's int32 labels are unchanged |
| Masks | roi_to_brain_from_atlas(roi_labels=[0, …]) painted label 0 with that parcel's value |
A 0 in roi_labels raises ValueError |
Label 0 is background and receives fill |
| Cluster reports | peaks took volume_mm3 and n_voxels from nilearn's 6-connected cluster sizes |
Peak rows carry their parent cluster's 26-connected extent | A join of peaks and clusters no longer reports two sizes for one cluster |
| Cluster reports | cluster_report(stat_threshold=t) kept voxels equal to t |
The threshold is exclusive | Matches get_clusters_table, which drives the peak table |
| Brain space | Voxel size was read off the affine's diagonal | Voxel size is the affine's column norms (nibabel.affines.voxel_sizes) |
A rotated or permuted isotropic MNI image is recognized instead of refused; axis-aligned affines are unchanged |
Renames and import paths¶
Old class names raise ImportError; moved modules raise ModuleNotFoundError.
Two find-and-replace passes cover the class renames:
| v0.5.1 import | v0.6.0 import |
|---|---|
from nltools.data import Brain_Data, Design_Matrix |
from nltools import BrainData, DesignMatrix |
from nltools.simulator import Simulator, SimulateGrid |
from nltools import Simulator, SimulateGrid |
from nltools.analysis import Roc |
from nltools import Roc |
from nltools.stats import … |
from nltools.algorithms import … |
from nltools.file_reader import onsets_to_dm |
DesignMatrix(events_path, …), or from nltools.io import events_to_dm |
from nltools.external import glover_hrf, spm_hrf, … |
from nilearn.glm.first_level import glover_hrf, spm_hrf, … |
from nltools.external import SRM, DetSRM |
BrainData.align(...), or from nltools.algorithms import align |
from nltools.utils import concatenate |
from nltools import concatenate |
from nltools.utils import get_resource_path |
from nltools.datasets import get_resource_path |
from nltools.utils import get_anatomical, all_same |
Removed |
from nltools.cross_validation import set_cv |
Removed — pass cv= and groups= to BrainData.predict |
from nltools.prefs import MNI_Template |
from nltools import set_brainspace, get_brainspace, reset_brainspace, with_brainspace |
from nltools.plotting import plot_brain, roc_plot, … |
Methods on the data classes; component_viewer is the one standalone plot left |
The four v0.5.1 mask functions keep their home in nltools.mask,
joined by roi_to_brain_from_atlas.
Examples¶
Every example below runs against the same simulated data.
import matplotlib
import numpy as np
matplotlib.use("Agg") # this page is rendered without a display
import matplotlib.pyplot as plt
from nltools import Adjacency, BrainData, DesignMatrix, Simulator
from nltools.mask import create_sphere
roi = create_sphere([0, -18, 18], radius=8)
brain = Simulator(random_state=0).create_data([0.0, 1.0] * 10, 1.0, reps=1).apply_mask(roi)
conditions = np.asarray(brain.Y).ravel()
print(brain.shape, conditions[:6])
(20, 81) [0. 1. 0. 1. 0. 1.]
Data classes¶
Shape and emptiness are properties rather than methods.
# v0.5.1: brain.shape(), brain.isempty(), brain.empty()
print(brain.shape, brain.is_empty, brain.create_empty().shape)
(20, 81) False (0,)
Adjacency.shape is the logical square shape; the packed vector length moved to
vector_shape.
rng = np.random.default_rng(1)
square = rng.normal(size=(10, 10))
square = (square + square.T) / 2
np.fill_diagonal(square, 0)
adjacency = Adjacency(square, matrix_type="similarity")
# v0.5.1: adjacency.shape() was (45,) and adjacency.square_shape() was (10, 10)
print(adjacency.shape, adjacency.vector_shape)
(10, 10) (45,)
Design matrices and GLM¶
DesignMatrix is backed by polars. Column assignment goes through
with_columns, and to_numpy() is the escape hatch. Generated columns —
intercepts, polynomial trends, cosine bases, spike regressors — carry the
reserved .nl_ prefix so they can never collide with a column of yours.
import polars as pl
design = DesignMatrix({"stim": np.tile([0.0, 1.0], 10)}, sampling_freq=0.5)
design = design.add_poly(1)
# v0.5.1: dm['stim_sq'] = dm['stim'] ** 2
design = design.with_columns((pl.col("stim") ** 2).alias("stim_sq"))
# v0.5.1: ['stim', 'stim_sq', 'poly_0', 'poly_1'] and dm.polys
print(design.columns, design.confounds)
['stim', '.nl_poly_0', '.nl_poly_1', 'stim_sq'] ['.nl_poly_0', '.nl_poly_1']
standardize(method='zscore') replaces zscore(). Called without columns=
it standardizes the non-confound columns only, where zscore() rescaled the
intercept and drift terms along with everything else.
# v0.5.1: design.zscore(columns=['stim'])
standardized = design.standardize(method="zscore", columns=["stim"])
print(dict(zip(standardized.columns, standardized.to_numpy().std(axis=0).round(2))))
{'stim': np.float64(0.97), '.nl_poly_0': np.float64(0.0), '.nl_poly_1': np.float64(0.61), 'stim_sq': np.float64(0.5)}
Convolution names the kernel with kernel=. The output column keeps the
_c{i} suffix it had in v0.5.1, and .convolved now lists those output names
rather than the source names.
# v0.5.1: DesignMatrix(...).convolve(conv_func='hrf'); .convolved was ['stim']
convolved = DesignMatrix({"stim": np.tile([0.0, 1.0], 10)}, sampling_freq=0.5).convolve()
print(convolved.columns, convolved.convolved)
['stim_c0'] ['stim_c0']
The kernel itself is nilearn's. convolve() calls
compute_regressor
at oversampling=50, and building a DesignMatrix from an events file calls
make_first_level_design_matrix, so both produce exactly what a nilearn
FirstLevelModel would. v0.5.1's kernel peaked around 8 s instead of 5 s at
TR = 2 (#492), so every
convolved regressor — and every beta, t and contrast downstream — changes. Array
kernels are unaffected.
Reading an events file is now the constructor's job; onsets_to_dm is gone.
from pathlib import Path
from nltools.datasets import get_resource_path
events = Path(get_resource_path()) / "onsets_example.csv"
# v0.5.1: onsets_to_dm(events, sampling_freq=0.5, run_length=200)
events_design = DesignMatrix(events, run_length=200, TR=2.0)
print(events_design.shape, events_design.columns[:3])
# Raw boxcars, for a design you convolve yourself (PPI, FIR)
print(DesignMatrix(events, run_length=200, TR=2.0, hrf_model=None).columns[:3])
(200, 13) ['BillyRiggins_c0', 'BuddyGarrity_c0', 'CoachTaylor_c0'] ['BillyRiggins', 'BuddyGarrity', 'CoachTaylor']
BrainData.regress() is gone. Fit the model, then ask for the contrasts you
want: t, p and standard errors are per contrast, not per regressor.
fit_design = DesignMatrix({"stim": conditions}, sampling_freq=0.5).add_poly(0)
# v0.5.1: brain.X = df; out = brain.regress(); out['beta'], out['t'], out['p']
fitted = brain.fit(model="glm", X=fit_design)
print(fitted.model.betas.shape, round(float(fitted.model.r2.data.mean()), 4))
result = fitted.compute_contrasts({"stim": [1, 0]}, inference=True)["stim"]
print(type(result).__name__, result.statistic.shape, result.degrees_of_freedom)
(2, 81) 0.2311 ContrastResult (81,) 18.0
The standalone OLS helper survives the move to nltools.algorithms but loses
mode=; the robust and ARMA fits are gone.
from nltools.algorithms import regress
X = np.column_stack([np.ones(20), conditions])
# v0.5.1: regress(X, Y, mode='ols')
betas, se, t, p, df, residuals = regress(X, rng.normal(size=(20, 5)))
print(betas.shape, df[0])
(2, 5) 18
Prediction¶
predict takes the labels, the estimator and the cross-validation scheme as
three separate keywords and returns a frozen PredictResult record. There is no
cv_dict and no set_cv: pass an integer (that many unshuffled stratified
folds), or any sklearn splitter, plus groups= when folds must respect subjects.
# v0.5.1: out = brain.predict(algorithm='svm', cv_dict={'type': 'kfolds', 'n_folds': 3})
predicted = brain.predict(y=conditions, estimator="linear_svc", cv=3)
print(type(predicted).__name__, predicted.weight_map.shape, len(predicted.scores))
PredictResult (81,) 3
spatial_scale replaces predict_multi: the same call runs whole-brain, per-ROI
or searchlight decoding. estimator='ridge' and 'ridge_classifier' now fit
RidgeCV/RidgeClassifierCV over a 1e-3…1e6 grid inside the per-fold scaler
pipeline instead of a single default penalty; pass
estimator_kwargs={'alphas': [1.0]} or your own sklearn object to pin one.
Roc spells its threshold rule method=, and its arguments are keyword-only.
from nltools import Roc
# v0.5.1: Roc(scores, labels, threshold_type='optimal_overall')
roc = Roc(
input_values=np.asarray(predicted.predictions, dtype=float),
binary_outcome=conditions.astype(bool),
method="optimal_overall",
)
roc.calculate()
print(round(roc.accuracy, 3), round(roc.auc, 3))
1.0 1.0
Similarity and RSA¶
The compared object is data on both classes, and the metric is metric=.
Adjacency.similarity renames perm_type= to method= and flips the diagonal
flag to include_diag=.
# v0.5.1: brain.similarity(image=brain[0], method='correlation')
print(np.round(brain.similarity(brain[0], metric="correlation")[:3], 3))
other = rng.normal(size=(10, 10))
other = (other + other.T) / 2
np.fill_diagonal(other, 0)
# v0.5.1: adjacency.similarity(other, perm_type='2d', ignore_diagonal=True)
out = adjacency.similarity(
Adjacency(other, matrix_type="similarity"),
method="2d",
metric="spearman",
include_diag=False,
n_permute=100,
random_state=0,
)
print({k: round(float(v), 3) for k, v in out.items()})
[ 1. -0.153 -0.085]
{'correlation': 0.059, 'p': 0.644}
cluster_summary moves the central tendency to summary= and the
within/between choice to scope=.
labels = ["left"] * 5 + ["right"] * 5
# v0.5.1: adjacency.cluster_summary(clusters=labels, metric='mean', summary='within')
summary = adjacency.cluster_summary(clusters=labels, summary="mean", scope="within")
print({k: round(v, 3) for k, v in summary.items()})
{'left': -0.211, 'right': -0.253}
For a searchlight or ROI RSA, BrainData.distance(spatial_scale='roi',
roi_mask=atlas) builds the Adjacency stack that the v0.5.1 workflow assembled
by hand, and nltools.mask.roi_to_brain_from_atlas paints the reduced values
back onto the atlas.
Alignment¶
Both alignment entry points now store transformation_matrix in one
orientation, transformed = original @ T, so back-projection is
transformed @ T.T everywhere. v0.5.1's BrainData.align stored the transpose
of this, contradicting its own docstring.
target = Simulator(random_state=1).create_data([0.0, 1.0] * 10, 1.0, reps=1).apply_mask(roi)
aligned = brain.align(target, method="procrustes")
# v0.5.1: aligned['transformed'].data @ aligned['transformation_matrix'].data
recovered = aligned["transformed"].data @ aligned["transformation_matrix"].data.T
centered = brain.data - brain.data.mean(axis=0)
print(round(float(np.corrcoef(recovered.ravel(), centered.ravel())[0, 1]), 6))
1.0
Shared response models arrive through the same method:
brain.align(target, method='probabilistic_srm') or 'deterministic_srm', and
nltools.algorithms.align aligns a list of subjects. The SRM and DetSRM
classes are no longer importable — the method builds them for you.
Statistics and inference¶
Everything from nltools.stats lives in nltools.algorithms. The four
permutation tests gained a _test suffix, everything after the data arguments is
keyword-only, and return_perms= became return_null=.
# v0.5.1: from nltools.stats import one_sample_permutation
# one_sample_permutation(x, n_permute=200, return_perms=True)
from nltools.algorithms import one_sample_permutation_test
print({k: round(v, 3) for k, v in one_sample_permutation_test(
rng.normal(size=50), n_permute=200, random_state=0
).items()})
{'mean': -0.135, 'p': 0.269}
ttest takes a popmean and an optional permutation test, and no longer
thresholds for you; nltools.algorithms.threshold is the separate step.
from nltools.algorithms import fdr, threshold
# v0.5.1: brain.ttest(threshold_dict={'fdr': .05})
t_test = brain.ttest(popmean=0.0)
print(sorted(t_test), t_test["t"].shape)
q = fdr(t_test["p"].data, q=0.05)
print(round(q, 5), threshold(t_test["t"], t_test["p"], thr=q).shape)
['mean', 'p', 't', 'z'] (81,) 0.00901 (81,)
Adjacency.ttest returns the same four maps as BrainData.ttest — the mean is
its own key, and t is the t-statistic rather than the edgewise mean.
Bootstrapping returns a BootstrapResult: an estimate, a standard error, and a
central percentile interval at confidence_level. save_weights= became
return_samples=.
# v0.5.1: out = brain.bootstrap('mean', n_samples=1000); out['Z'], out['p']
boot = brain.bootstrap("mean", n_samples=1000, confidence_level=0.95, random_state=0)
print(type(boot).__name__, boot.estimate.shape, round(float(boot.ci_lower.mean()), 3))
BootstrapResult (81,) 0.038
Intersubject correlation is a function, not an Adjacency method:
nltools.algorithms.isc, isc_group, isfc and isps take arrays and return
dictionaries.
Plotting¶
The standalone plotters are gone; each class draws itself, and the axis keyword
is ax everywhere, matching matplotlib.
# v0.5.1: design.heatmap(); adjacency.plot(limit=1, axes=ax)
figure = design.plot()
adjacency.plot(limit=1)
# v0.5.1: plot_brain(brain.mean(), how='glass', thr_upper=1)
brain.mean().plot(method="glass", upper=1)
plt.close("all")
print(type(figure).__name__)
Figure
BrainData.plot renames threshold_upper/threshold_lower to upper/lower
and moves v0.5.1's view='glass'|'mni'|'full' onto method=; view= is now the
slice axis. iplot is rebuilt on niivue and takes view=, threshold=,
atlas= and an autoscale/symmetric display pair.
component_viewer is the only standalone plotting function
left.
IO and datasets¶
The bundled-dataset fetchers cache to a shared location of their own, so
data_dir= and resume= are gone and verbose is quiet by default. These two
examples download data, so they are not executed when this page is built.
from nltools.datasets import fetch_pain, fetch_neurovault_collection
# v0.5.1: fetch_pain(data_dir='~/nltools_data', resume=True, verbose=1)
pain = fetch_pain()
# v0.5.1: download_collection(collection=504); get_collection_image_metadata(504)
metadata, files = fetch_neurovault_collection(504)
Atlases and bundled resources are looked up by name with
load_atlas, list_atlases, fetch_resource and list_resources.
HDF5 files written by 0.5.1 or earlier no longer load: export them to NIfTI or
CSV under 0.5.1 first, then read them back in 0.6.0.
Removals and their replacements¶
| v0.5.1 | What to do instead |
|---|---|
Groupby, Brain_Data.groupby(), Brain_Data.aggregate() |
Removed, no successor — extract each region with BrainData.extract_roi and work with the returned array |
Design_Matrix_Series |
Removed, no successor — indexing a DesignMatrix column returns a polars.Series |
set_cv |
Pass cv=<int> or an sklearn splitter to BrainData.predict, with groups= when folds must hold out subjects |
Roc(threshold_type=…) |
Roc(method=…), keyword-only |
plot_brain, plot_t_brain |
BrainData.plot(method='glass'\|'mni'\|'full'); run the t-test yourself with BrainData.ttest |
plot_interactive_brain |
Removed, no successor — BrainData.iplot |
plot_stacked_adjacency |
Adjacency.plot_stacked(data, ...) — the same two triangles, plus titles, node labels and a per-triangle color scale |
fetch_pain(data_dir=…, resume=…, verbose=1) |
fetch_pain(verbose=0) — caching is internal |
Brain_Data.icc(), compute_icc |
Removed, no successor — pingouin.intraclass_corr on brain.data |
double_center, u_center |
Removed — internal steps of nltools.algorithms.distance_correlation |
Brain_Data.nifti_masker |
nilearn.masking.apply_mask(img, brain.mask) |
summarize_bootstrap, regress_permutation |
The bootstrap methods return a BootstrapResult; permute with nltools.algorithms.one_sample_permutation_test |
nltools.utils.all_same, get_anatomical, set_algorithm |
Removed — numpy, nilearn.datasets, and the estimator= keyword cover them |
What did not change¶
BrainData, Adjacency and DesignMatrix still mean what they meant in v0.5.1,
and the methods that were already well named — threshold, detrend, filter,
r_to_z, distance, to_nifti, write — keep their names and their results.
Masks are still nibabel images, BrainData.data is still a 2-D NumPy array of
images by voxels, Y and X still hold labels and designs, and
KFoldStratified and the four v0.5.1 functions in
nltools.mask are unchanged. Reading a NIfTI file, a list of
files, or a NeuroVault URL into BrainData works as before, and now accepts
Path objects too.