Skip to content

stats

tit.stats

Cluster-based permutation testing for TI-Toolbox.

Provides group comparison and correlation analyses with cluster-based permutation correction for multiple comparisons. Both workflows produce NIfTI output maps, diagnostic plots, and text summaries written to the BIDS derivatives tree.

Public API

run_group_comparison Two-group voxelwise comparison with cluster-based permutation correction. run_correlation Voxelwise brain-behavior correlation with cluster-based permutation correction (ACES-style). GroupComparisonConfig Configuration dataclass for group comparison. GroupComparisonResult Result container for group comparison. CorrelationConfig Configuration dataclass for correlation analysis. CorrelationResult Result container for correlation analysis.

See Also

tit.analyzer : Single-subject ROI-level field analysis.

CorrelationConfig dataclass

CorrelationConfig(analysis_name: str, subjects: list[Subject], correlation_type: CorrelationType = PEARSON, cluster_threshold: float = 0.05, cluster_stat: ClusterStat = MASS, n_permutations: int = 1000, alpha: float = 0.05, n_jobs: int = -1, seed: int | None = None, use_weights: bool = True, tissue_type: TissueType = GREY, nifti_file_pattern: str | None = None, space: AnalysisSpace = MNI, fsaverage_field: str = 'TI_max', fsaverage_spacing: int = 5, effect_metric: str = 'Effect Size', field_metric: str = 'Electric Field Magnitude', atlas_files: list[str] = list())

Configuration for correlation-based cluster permutation testing.

Tests voxelwise correlation between brain field intensities and a continuous behavioral or clinical measure (effect size) across subjects, with cluster-based permutation correction for multiple comparisons.

Attributes

analysis_name : str Human-readable name for this analysis run. subjects : list of Subject Subject entries with associated effect sizes. correlation_type : CorrelationType Pearson or Spearman rank correlation. cluster_threshold : float Uncorrected p-value threshold for forming clusters. cluster_stat : ClusterStat Cluster-level statistic used for permutation testing ("mass" or "size"). n_permutations : int Number of permutations for the null distribution. alpha : float Family-wise error rate for significance. n_jobs : int Number of parallel workers (-1 for the global CPU limit -- :func:tit.cpu.cpu_limit; larger values are clamped to it). seed : int or None Seed of the permutation generator; see :class:GroupComparisonConfig. use_weights : bool Whether to apply per-subject weights during correlation. Default True. tissue_type : TissueType or str Which tissue compartment to analyze: "grey" (default), "white" or "all". nifti_file_pattern : str or None Filename pattern for subject NIfTI files. If None, derived automatically from tissue_type. space : AnalysisSpace or str "mni" (default) or "fsaverage"; see :class:GroupComparisonConfig. fsaverage_field : str Surface field for space="fsaverage". Default "TI_max". fsaverage_spacing : int fsaverage ico spacing (5, 6 or 7). Default 5. effect_metric : str Label for the behavioral/clinical variable in plots. field_metric : str Label for the field intensity axis in plots. atlas_files : list of str Atlas filenames for overlap analysis (looked up in the bundled atlas directory).

Raises

ValueError If fewer than 3 subjects are given, if an effect size is not a finite number or every effect size is equal (the correlation is undefined), if use_weights is on and a weight is not a positive finite number, or for the alpha/cluster_threshold/ n_permutations rules of :class:GroupComparisonConfig.

See Also

CorrelationResult : Result container returned by the analysis. run_correlation : Orchestration function that consumes this config.

CorrelationType

Bases: StrEnum

Type of correlation coefficient to compute.

Subject dataclass

Subject(subject_id: str, simulation_name: str, effect_size: float, weight: float = 1.0)

A single subject in a correlation analysis.

Attributes

subject_id : str Subject identifier (without sub- prefix). simulation_name : str Name of the simulation to load for this subject. effect_size : float Continuous behavioral or clinical measure to correlate with field intensity. weight : float Per-subject weight (default 1.0).

load_subjects classmethod

load_subjects(csv_path: str) -> list[Subject]

Load correlation subjects from a CSV file.

Expected columns: subject_id, simulation_name, effect_size. Optional column: weight. Rows with NaN subject_id or effect_size are silently skipped. The sub- prefix is stripped from subject IDs automatically.

Parameters

csv_path : str Path to a CSV file with the required columns.

Returns

list of Subject Subject instances parsed from valid CSV rows.

Raises

ValueError If required columns are missing, a kept row has no simulation_name, or no valid subjects are found.

Source code in tit/stats/config.py
@classmethod
def load_subjects(cls, csv_path: str) -> list["CorrelationConfig.Subject"]:
    """Load correlation subjects from a CSV file.

    Expected columns: ``subject_id``, ``simulation_name``,
    ``effect_size``.  Optional column: ``weight``.  Rows with NaN
    ``subject_id`` or ``effect_size`` are silently skipped.  The ``sub-``
    prefix is stripped from subject IDs automatically.

    Parameters
    ----------
    csv_path : str
        Path to a CSV file with the required columns.

    Returns
    -------
    list of Subject
        Subject instances parsed from valid CSV rows.

    Raises
    ------
    ValueError
        If required columns are missing, a kept row has no simulation_name, or
        no valid subjects are found.
    """
    subjects = []
    for line, row in _read_participant_rows(
        csv_path, {"subject_id", "simulation_name", "effect_size"}
    ):
        if row["subject_id"] is None or row["effect_size"] is None:
            continue
        if row["simulation_name"] is None:
            raise ValueError(f"CSV row {line}: simulation_name is required")
        weight = row.get("weight")
        subjects.append(
            cls.Subject(
                subject_id=_participant_id(row["subject_id"]),
                simulation_name=row["simulation_name"],
                effect_size=float(row["effect_size"]),
                weight=1.0 if weight is None else float(weight),
            )
        )

    if not subjects:
        raise ValueError("No valid subjects found in CSV")
    return subjects

CorrelationResult dataclass

CorrelationResult(success: bool, output_dir: str, n_subjects: int, n_significant_voxels: int, n_significant_clusters: int, cluster_threshold: float, analysis_time: float, clusters: list, log_file: str)

Result of a correlation-based cluster permutation test.

Attributes

success : bool Whether the analysis completed without error. output_dir : str Absolute path to the directory containing all outputs (NIfTI maps, plots, summary text, log). n_subjects : int Number of subjects included in the analysis. n_significant_voxels : int Total voxels surviving cluster-corrected threshold. n_significant_clusters : int Number of spatially contiguous clusters that survived permutation correction. cluster_threshold : float Cluster-level statistic threshold derived from the permutation null distribution at the requested alpha. analysis_time : float Wall-clock duration of the full analysis in seconds. clusters : list of dict One entry per significant cluster, containing size, mass, peak coordinates, mean/peak correlation coefficients, and atlas overlap info. log_file : str Absolute path to the analysis log file.

See Also

CorrelationConfig : Configuration that produced this result. run_correlation : Function that returns this result.

GroupComparisonConfig dataclass

GroupComparisonConfig(analysis_name: str, subjects: list[Subject], test_type: TestType = UNPAIRED, alternative: Alternative = TWO_SIDED, cluster_threshold: float = 0.05, cluster_stat: ClusterStat = MASS, n_permutations: int = 1000, alpha: float = 0.05, n_jobs: int = -1, seed: int | None = None, tissue_type: TissueType = GREY, nifti_file_pattern: str | None = None, space: AnalysisSpace = MNI, fsaverage_field: str = 'TI_max', fsaverage_spacing: int = 5, group1_name: str = 'Responders', group2_name: str = 'Non-Responders', value_metric: str = 'Electric Field Magnitude', atlas_files: list[str] = list())

Configuration for cluster-based permutation testing between two groups.

Compares voxelwise field intensities between responders and non-responders using a t-test with cluster-based permutation correction for multiple comparisons.

Attributes

analysis_name : str Human-readable name for this analysis run. subjects : list of Subject Subject entries, each labelled as responder (1) or non-responder (0). test_type : TestType or str "unpaired" (default) or "paired" t-test. Strings are coerced to :class:TestType. alternative : Alternative or str Sidedness: "two-sided" (default), "greater" or "less". cluster_threshold : float Uncorrected p-value threshold for forming clusters. Default 0.05. cluster_stat : ClusterStat or str Cluster-level statistic used for permutation testing, "mass" (default) or "size". n_permutations : int Number of permutations for the null distribution. Default 1000; the smallest reportable p-value is 1 / (n + 1). alpha : float Family-wise error rate for significance. Default 0.05. n_jobs : int Number of parallel workers (-1, the default, for the global CPU limit -- :func:tit.cpu.cpu_limit; larger values are clamped to it). seed : int or None Seed of the permutation generator; None (default) draws a fresh one. Either way the seed used is written in the run's summary (analysis_summary.txt; surface_stats.json for fsaverage). tissue_type : TissueType or str Which tissue compartment to analyze: "grey" (default), "white" or "all". Ignored when space is "fsaverage". nifti_file_pattern : str or None Filename pattern for subject NIfTI files, with a {simulation_name} placeholder. If None (default), derived from tissue_type, e.g. "grey_{simulation_name}_TI_MNI_MNI_TI_max.nii.gz". space : AnalysisSpace or str Where the statistics run: "mni" (default, voxelwise on the MNI-space NIfTIs) or "fsaverage" (vertexwise on the per-subject fsaverage projections written by the simulator). fsaverage_field : str Surface field for space="fsaverage"; one of :data:tit.constants.FSAVG_FIELD_NAMES ("TI_max", "TI_normal", "hf_peak", "hf_rms"). Default "TI_max". fsaverage_spacing : int fsaverage ico spacing for space="fsaverage": 5 (default), 6 or 7. group1_name : str Display label for the responder group. Default "Responders". group2_name : str Display label for the non-responder group. Default "Non-Responders". value_metric : str Label for the field values in the summary. Default "Electric Field Magnitude". atlas_files : list of str Atlas filenames for overlap analysis (looked up in the bundled atlas directory). Default empty.

Raises

ValueError If subjects lacks at least one responder and one non-responder, if a response is not 0 or 1, if a paired test has unequal group sizes, if alpha or cluster_threshold is not strictly between 0 and 1, if n_permutations is not a whole number or is below floor(1 / alpha) (20 at alpha 0.05: with fewer, no p-value could fall below alpha), if a string value is not a member of its enum, or if fsaverage_field/fsaverage_spacing are invalid for space="fsaverage".

Examples

from tit.stats import GroupComparisonConfig subjects = [ ... GroupComparisonConfig.Subject("ernie", "L_Insula", response=1), ... GroupComparisonConfig.Subject("101", "L_Insula", response=0), ... ] cfg = GroupComparisonConfig( ... analysis_name="active_vs_sham", subjects=subjects, ... test_type="unpaired", alternative="two-sided", ... cluster_stat="mass", n_permutations=1000, tissue_type="grey", ... ) cfg.test_type is GroupComparisonConfig.TestType.UNPAIRED True cfg.nifti_file_pattern 'grey_{simulation_name}_TI_MNI_MNI_TI_max.nii.gz'

Subjects can also come from a CSV with columns subject_id, simulation_name, response:

subjects = GroupComparisonConfig.load_subjects("subjects.csv") # doctest: +SKIP

See Also

GroupComparisonResult : Result container returned by the analysis. run_group_comparison : Orchestration function that consumes this config.

TestType

Bases: StrEnum

Type of statistical test for group comparison.

Attributes

UNPAIRED : str "unpaired" -- independent-samples t-test. PAIRED : str "paired" -- paired-samples t-test (groups must be the same size and ordered pairwise).

Alternative

Bases: StrEnum

Sidedness of the test hypothesis.

Attributes

TWO_SIDED : str "two-sided". GREATER : str "greater" -- responders > non-responders. LESS : str "less" -- responders < non-responders.

Subject dataclass

Subject(subject_id: str, simulation_name: str, response: int)

A single subject in a group comparison analysis.

Attributes

subject_id : str Subject identifier (without sub- prefix). simulation_name : str Name of the simulation to load for this subject. response : int Group label -- 1 for responder, 0 for non-responder.

load_subjects classmethod

load_subjects(csv_path: str) -> list[Subject]

Load group comparison subjects from a CSV file.

Expected columns: subject_id, simulation_name, response (0 or 1). The sub- prefix is stripped from subject IDs automatically.

Parameters

csv_path : str Path to a CSV file with the required columns.

Returns

list of Subject Subject instances parsed from the CSV rows.

Raises

ValueError If required columns are missing, or a row lacks one of their values.

Source code in tit/stats/config.py
@classmethod
def load_subjects(cls, csv_path: str) -> list["GroupComparisonConfig.Subject"]:
    """Load group comparison subjects from a CSV file.

    Expected columns: ``subject_id``, ``simulation_name``, ``response``
    (0 or 1).  The ``sub-`` prefix is stripped from subject IDs
    automatically.

    Parameters
    ----------
    csv_path : str
        Path to a CSV file with the required columns.

    Returns
    -------
    list of Subject
        Subject instances parsed from the CSV rows.

    Raises
    ------
    ValueError
        If required columns are missing, or a row lacks one of their values.
    """
    subjects = []
    for line, row in _read_participant_rows(
        csv_path, {"subject_id", "simulation_name", "response"}
    ):
        if None in (row["subject_id"], row["simulation_name"], row["response"]):
            raise ValueError(
                f"CSV row {line}: subject_id, simulation_name and response are "
                "all required"
            )
        subjects.append(
            cls.Subject(
                subject_id=_participant_id(row["subject_id"]),
                simulation_name=row["simulation_name"],
                response=int(float(row["response"])),
            )
        )
    return subjects

GroupComparisonResult dataclass

GroupComparisonResult(success: bool, output_dir: str, n_responders: int, n_non_responders: int, n_significant_voxels: int, n_significant_clusters: int, cluster_threshold: float, analysis_time: float, clusters: list, log_file: str)

Result of a group comparison permutation test.

Attributes

success : bool Whether the analysis completed without error. output_dir : str Absolute path to the directory containing all outputs (NIfTI maps, plots, summary text, log). n_responders : int Number of responder subjects included. n_non_responders : int Number of non-responder subjects included. n_significant_voxels : int Total voxels surviving cluster-corrected threshold. n_significant_clusters : int Number of spatially contiguous clusters that survived permutation correction. cluster_threshold : float Cluster-level statistic threshold derived from the permutation null distribution at the requested alpha. analysis_time : float Wall-clock duration of the full analysis in seconds. clusters : list of dict One entry per significant cluster, containing size, mass, peak coordinates, and atlas overlap info. log_file : str Absolute path to the analysis log file.

See Also

GroupComparisonConfig : Configuration that produced this result. run_group_comparison : Function that returns this result.

run_correlation

run_correlation(config: CorrelationConfig, callback_handler: Handler | None = None, stop_callback: Callable[[], bool] | None = None) -> CorrelationResult

Run cluster-based permutation testing for a brain-behaviour correlation.

Correlates each voxel's field value with a continuous per-subject measure (Subject.effect_size), Pearson or Spearman, with cluster-based permutation correction (ACES-style). Outputs go under <project>/derivatives/ti-toolbox/stats/correlation/<analysis_name>/.

Parameters

config : CorrelationConfig Fully specified correlation configuration. callback_handler : logging.Handler or None, optional Extra handler attached to the run-scoped logger. stop_callback : callable or None, optional Zero-argument callable returning True to request early termination.

Returns

CorrelationResult Output directory, subject count, significant voxel/cluster counts, cluster threshold (NaN when no cluster formed at the cluster-forming threshold, so no permutation ran) and per-cluster details.

Raises

KeyboardInterrupt If stop_callback returns True during execution. FileNotFoundError If a subject's NIfTI is missing.

Notes

Re-using an analysis_name replaces the earlier run, as in :func:run_group_comparison: its folder is deleted before the inputs load, so a re-run that then fails leaves nothing under that name.

Examples

from tit.stats import CorrelationConfig, run_correlation subjects = CorrelationConfig.load_subjects("subjects_continuous.csv") # doctest: +SKIP cfg = CorrelationConfig(analysis_name="dose_response", subjects=subjects, ... correlation_type="spearman") # doctest: +SKIP run_correlation(cfg).n_significant_clusters # doctest: +SKIP 1

See Also

CorrelationConfig : The configuration consumed here. run_group_comparison : Two-group comparison with the same correction.

Source code in tit/stats/permutation.py
def run_correlation(
    config: CorrelationConfig,
    callback_handler: logging.Handler | None = None,
    stop_callback: Callable[[], bool] | None = None,
) -> CorrelationResult:
    """Run cluster-based permutation testing for a brain-behaviour correlation.

    Correlates each voxel's field value with a continuous per-subject
    measure (``Subject.effect_size``), Pearson or Spearman, with
    cluster-based permutation correction (ACES-style).  Outputs go under
    ``<project>/derivatives/ti-toolbox/stats/correlation/<analysis_name>/``.

    Parameters
    ----------
    config : CorrelationConfig
        Fully specified correlation configuration.
    callback_handler : logging.Handler or None, optional
        Extra handler attached to the run-scoped logger.
    stop_callback : callable or None, optional
        Zero-argument callable returning ``True`` to request early
        termination.

    Returns
    -------
    CorrelationResult
        Output directory, subject count, significant voxel/cluster counts,
        cluster threshold (NaN when no cluster formed at the cluster-forming
        threshold, so no permutation ran) and per-cluster details.

    Raises
    ------
    KeyboardInterrupt
        If *stop_callback* returns ``True`` during execution.
    FileNotFoundError
        If a subject's NIfTI is missing.

    Notes
    -----
    Re-using an ``analysis_name`` replaces the earlier run, as in
    :func:`run_group_comparison`: its folder is deleted before the inputs
    load, so a re-run that then fails leaves nothing under that name.

    Examples
    --------
    >>> from tit.stats import CorrelationConfig, run_correlation
    >>> subjects = CorrelationConfig.load_subjects("subjects_continuous.csv")  # doctest: +SKIP
    >>> cfg = CorrelationConfig(analysis_name="dose_response", subjects=subjects,
    ...                         correlation_type="spearman")  # doctest: +SKIP
    >>> run_correlation(cfg).n_significant_clusters  # doctest: +SKIP
    1

    See Also
    --------
    CorrelationConfig : The configuration consumed here.
    run_group_comparison : Two-group comparison with the same correction.
    """
    try:
        return _run_correlation_inner(config, callback_handler, stop_callback)
    except KeyboardInterrupt:
        raise
    except Exception as exc:
        _log_failure("correlation", exc)
        raise

run_group_comparison

run_group_comparison(config: GroupComparisonConfig, callback_handler: Handler | None = None, stop_callback: Callable[[], bool] | None = None) -> GroupComparisonResult

Run cluster-based permutation testing for a two-group comparison.

Loads responder and non-responder field maps, performs voxelwise (or, for config.space == "fsaverage", vertexwise) t-tests, applies cluster-based permutation correction, generates diagnostic plots, and saves all outputs under <project>/derivatives/ti-toolbox/stats/group_comparison/<analysis_name>/.

Parameters

config : GroupComparisonConfig Fully specified group comparison configuration. callback_handler : logging.Handler or None, optional Extra handler attached to the run-scoped logger (used by the app to stream log lines). stop_callback : callable or None, optional Zero-argument callable returning True to request early termination. Checked between pipeline stages.

Returns

GroupComparisonResult Summary of the outcome: output directory, subject counts, significant voxel/cluster counts, the permutation-derived cluster threshold (NaN when no cluster formed at the cluster-forming threshold, so no permutation ran) and the per-cluster details.

Raises

KeyboardInterrupt If stop_callback returns True during execution. ValueError If the subjects' NIfTIs do not share one grid (the message names the offending subject). FileNotFoundError If a subject's MNI-space NIfTI (per config.nifti_file_pattern) or fsaverage projection is missing.

Notes

Permutation p-values are (b + 1) / (m + 1), so with 1000 permutations the floor is 1/1001; a result is never exactly 0.

Re-using an analysis_name replaces the earlier run: its folder is deleted before the inputs load, together with any file put there by hand, so a re-run that then fails leaves nothing under that name. The app asks before it submits such a run; a script does not, so choose a new name to keep an earlier result.

Examples

from tit.stats import GroupComparisonConfig, run_group_comparison subjects = GroupComparisonConfig.load_subjects("subjects.csv") # doctest: +SKIP cfg = GroupComparisonConfig( ... analysis_name="active_vs_sham", subjects=subjects, ... test_type="unpaired", n_permutations=1000, tissue_type="grey", ... ) # doctest: +SKIP res = run_group_comparison(cfg) # doctest: +SKIP res.success, res.n_significant_clusters, res.output_dir # doctest: +SKIP (True, 2, '.../derivatives/ti-toolbox/stats/group_comparison/active_vs_sham')

See Also

GroupComparisonConfig : The configuration consumed here. GroupComparisonResult : The returned container. run_correlation : Brain-behaviour correlation with the same correction.

Source code in tit/stats/permutation.py
def run_group_comparison(
    config: GroupComparisonConfig,
    callback_handler: logging.Handler | None = None,
    stop_callback: Callable[[], bool] | None = None,
) -> GroupComparisonResult:
    """Run cluster-based permutation testing for a two-group comparison.

    Loads responder and non-responder field maps, performs voxelwise (or,
    for ``config.space == "fsaverage"``, vertexwise) t-tests, applies
    cluster-based permutation correction, generates diagnostic plots, and
    saves all outputs under
    ``<project>/derivatives/ti-toolbox/stats/group_comparison/<analysis_name>/``.

    Parameters
    ----------
    config : GroupComparisonConfig
        Fully specified group comparison configuration.
    callback_handler : logging.Handler or None, optional
        Extra handler attached to the run-scoped logger (used by the app
        to stream log lines).
    stop_callback : callable or None, optional
        Zero-argument callable returning ``True`` to request early
        termination.  Checked between pipeline stages.

    Returns
    -------
    GroupComparisonResult
        Summary of the outcome: output directory, subject counts,
        significant voxel/cluster counts, the permutation-derived cluster
        threshold (NaN when no cluster formed at the cluster-forming
        threshold, so no permutation ran) and the per-cluster details.

    Raises
    ------
    KeyboardInterrupt
        If *stop_callback* returns ``True`` during execution.
    ValueError
        If the subjects' NIfTIs do not share one grid (the message names
        the offending subject).
    FileNotFoundError
        If a subject's MNI-space NIfTI (per ``config.nifti_file_pattern``)
        or fsaverage projection is missing.

    Notes
    -----
    Permutation p-values are ``(b + 1) / (m + 1)``, so with 1000
    permutations the floor is ``1/1001``; a result is never exactly 0.

    Re-using an ``analysis_name`` replaces the earlier run: its folder is
    deleted before the inputs load, together with any file put there by
    hand, so a re-run that then fails leaves nothing under that name. The
    app asks before it submits such a run; a script does not, so choose a
    new name to keep an earlier result.

    Examples
    --------
    >>> from tit.stats import GroupComparisonConfig, run_group_comparison
    >>> subjects = GroupComparisonConfig.load_subjects("subjects.csv")  # doctest: +SKIP
    >>> cfg = GroupComparisonConfig(
    ...     analysis_name="active_vs_sham", subjects=subjects,
    ...     test_type="unpaired", n_permutations=1000, tissue_type="grey",
    ... )  # doctest: +SKIP
    >>> res = run_group_comparison(cfg)  # doctest: +SKIP
    >>> res.success, res.n_significant_clusters, res.output_dir  # doctest: +SKIP
    (True, 2, '.../derivatives/ti-toolbox/stats/group_comparison/active_vs_sham')

    See Also
    --------
    GroupComparisonConfig : The configuration consumed here.
    GroupComparisonResult : The returned container.
    run_correlation : Brain-behaviour correlation with the same correction.
    """
    from tit.telemetry import track_operation
    from tit import constants as _const

    with track_operation(_const.TELEMETRY_OP_STATS):
        try:
            if config.space == GroupComparisonConfig.AnalysisSpace.FSAVERAGE:
                from tit.stats.surface import run_surface_group_comparison

                return run_surface_group_comparison(
                    config, callback_handler, stop_callback
                )
            return _run_group_comparison_inner(config, callback_handler, stop_callback)
        except KeyboardInterrupt:
            raise
        except Exception as exc:
            _log_failure("group_comparison", exc)
            raise