Skip to content

group

tit.analyzer.group

Multi-subject group analysis.

Runs per-subject ROI analyses in-process via :class:Analyzer, aggregates the results into a summary CSV with an AVERAGE row, and produces a 2x2 comparison bar-chart saved as PDF.

Public API

GroupResult Container for multi-subject group analysis outcomes. run_group_analysis Run the same ROI analysis across multiple subjects and summarise.

See Also

tit.analyzer.analyzer : Single-subject analyzer used per-subject.

GroupResult dataclass

GroupResult(subject_results: dict[str, AnalysisResult], summary_csv_path: Path, comparison_plot_path: Path | None)

Outcome of a multi-subject group analysis.

Attributes

subject_results : dict of str to AnalysisResult Mapping of subject ID to its :class:~tit.analyzer.analyzer.AnalysisResult. summary_csv_path : pathlib.Path Path to the summary CSV (one row per subject plus an AVERAGE row). comparison_plot_path : pathlib.Path or None Path to the 2x2 comparison bar-chart PDF, or None if plotting failed.

See Also

run_group_analysis : Factory function that produces this result. AnalysisResult : Per-subject analysis container.

run_group_analysis

run_group_analysis(subject_ids: list[str], simulation: str, space: str = 'mesh', tissue_type: str = 'GM', analysis_type: str = 'spherical', center: tuple[float, float, float] | None = None, radius: float | None = None, coordinate_space: str = 'subject', spheres: Sequence[tuple[float, float, float, float]] | None = None, atlas: str | None = None, region: str | list[str] | None = None, visualize: bool = False, output_dir: str | Path | None = None, field: str | None = None, mask_path: str | None = None) -> GroupResult

Run the same ROI analysis across multiple subjects and summarise.

Dispatches to analyze_sphere or analyze_cortex on each subject, builds a summary CSV (with an AVERAGE row), and generates a 2x2 comparison bar-chart PDF.

Parameters

subject_ids : list of str Subject identifiers (without sub- prefix). simulation : str Simulation (montage) folder name, shared by all subjects. space : str, optional "mesh" or "voxel". Default "mesh". tissue_type : str, optional "GM", "WM", or "both" (voxel only). Default "GM". analysis_type : str, optional "spherical", "cortical" or "mask". Default "spherical". center : tuple of float or None, optional (x, y, z) sphere centre; required when analysis_type is "spherical". radius : float or None, optional Sphere radius in mm; required when analysis_type is "spherical". coordinate_space : str, optional "subject" or "MNI" (spherical and mask). Default "subject"; a group mask analysis requires "MNI". spheres : sequence of tuple of float or None, optional One (x, y, z, r) per sphere, unioned into a single ROI (spherical only). Takes precedence over center/radius. atlas : str or None, optional Atlas name (cortical only). region : str, list of str, or None, optional Region name or list of region names (cortical only). visualize : bool, optional Generate per-subject visualization artifacts. Default False. output_dir : str, pathlib.Path, or None, optional Override output directory. If None, derived from PathManager. field : str or None, optional Field to analyze (constants.FIELD_REGISTRY name, e.g. "TI_max", "TI_normal", "hf_peak"). Default None resolves the TI_max/mTI_max envelope per subject. mask_path : str or None, optional MNI-space NIfTI mask (analysis_type="mask" only).

Returns

GroupResult Per-subject results, the summary CSV path (group_summary.csv), and the comparison plot path.

Raises

KeyError If analysis_type is not "spherical", "cortical" or "mask". ValueError If analysis_type="mask" with a non-MNI coordinate_space or an unreadable mask_path. FileNotFoundError If a subject has no field file for simulation in space.

Examples

from tit.analyzer import run_group_analysis res = run_group_analysis( ... subject_ids=["ernie", "101"], simulation="L_Insula", space="voxel", ... analysis_type="spherical", center=(-35.0, 5.0, 5.0), radius=10.0, ... coordinate_space="MNI", ... ) # doctest: +SKIP res.summary_csv_path.name, sorted(res.subject_results) # doctest: +SKIP ('group_summary.csv', ['101', 'ernie']) run_group_analysis(["ernie", "101"], "L_Insula", space="mesh", ... analysis_type="cortical", atlas="DK40", ... region="lh.insula") # doctest: +SKIP

See Also

Analyzer : Single-subject analyzer used internally per subject. GroupResult : Container for the returned outcomes.

Source code in tit/analyzer/group.py
def run_group_analysis(
    subject_ids: list[str],
    simulation: str,
    space: str = "mesh",
    tissue_type: str = "GM",
    analysis_type: str = "spherical",
    center: tuple[float, float, float] | None = None,
    radius: float | None = None,
    coordinate_space: str = "subject",
    spheres: Sequence[tuple[float, float, float, float]] | None = None,
    atlas: str | None = None,
    region: str | list[str] | None = None,
    visualize: bool = False,
    output_dir: str | Path | None = None,
    field: str | None = None,
    mask_path: str | None = None,
) -> GroupResult:
    """Run the same ROI analysis across multiple subjects and summarise.

    Dispatches to ``analyze_sphere`` or ``analyze_cortex`` on each subject,
    builds a summary CSV (with an AVERAGE row), and generates a 2x2
    comparison bar-chart PDF.

    Parameters
    ----------
    subject_ids : list of str
        Subject identifiers (without ``sub-`` prefix).
    simulation : str
        Simulation (montage) folder name, shared by all subjects.
    space : str, optional
        ``"mesh"`` or ``"voxel"``. Default ``"mesh"``.
    tissue_type : str, optional
        ``"GM"``, ``"WM"``, or ``"both"`` (voxel only). Default ``"GM"``.
    analysis_type : str, optional
        ``"spherical"``, ``"cortical"`` or ``"mask"``. Default
        ``"spherical"``.
    center : tuple of float or None, optional
        ``(x, y, z)`` sphere centre; required when *analysis_type* is
        ``"spherical"``.
    radius : float or None, optional
        Sphere radius in mm; required when *analysis_type* is
        ``"spherical"``.
    coordinate_space : str, optional
        ``"subject"`` or ``"MNI"`` (spherical and mask). Default
        ``"subject"``; a group mask analysis requires ``"MNI"``.
    spheres : sequence of tuple of float or None, optional
        One ``(x, y, z, r)`` per sphere, unioned into a single ROI
        (spherical only). Takes precedence over *center*/*radius*.
    atlas : str or None, optional
        Atlas name (cortical only).
    region : str, list of str, or None, optional
        Region name or list of region names (cortical only).
    visualize : bool, optional
        Generate per-subject visualization artifacts. Default ``False``.
    output_dir : str, pathlib.Path, or None, optional
        Override output directory. If ``None``, derived from PathManager.
    field : str or None, optional
        Field to analyze (``constants.FIELD_REGISTRY`` name, e.g.
        ``"TI_max"``, ``"TI_normal"``, ``"hf_peak"``). Default ``None``
        resolves the TI_max/mTI_max envelope per subject.
    mask_path : str or None, optional
        MNI-space NIfTI mask (``analysis_type="mask"`` only).

    Returns
    -------
    GroupResult
        Per-subject results, the summary CSV path
        (``group_summary.csv``), and the comparison plot path.

    Raises
    ------
    KeyError
        If *analysis_type* is not ``"spherical"``, ``"cortical"`` or
        ``"mask"``.
    ValueError
        If ``analysis_type="mask"`` with a non-MNI *coordinate_space* or an
        unreadable *mask_path*.
    FileNotFoundError
        If a subject has no field file for *simulation* in *space*.

    Examples
    --------
    >>> from tit.analyzer import run_group_analysis
    >>> res = run_group_analysis(
    ...     subject_ids=["ernie", "101"], simulation="L_Insula", space="voxel",
    ...     analysis_type="spherical", center=(-35.0, 5.0, 5.0), radius=10.0,
    ...     coordinate_space="MNI",
    ... )  # doctest: +SKIP
    >>> res.summary_csv_path.name, sorted(res.subject_results)  # doctest: +SKIP
    ('group_summary.csv', ['101', 'ernie'])
    >>> run_group_analysis(["ernie", "101"], "L_Insula", space="mesh",
    ...                    analysis_type="cortical", atlas="DK40",
    ...                    region="lh.insula")  # doctest: +SKIP

    See Also
    --------
    Analyzer : Single-subject analyzer used internally per subject.
    GroupResult : Container for the returned outcomes.
    """
    from tit.telemetry import track_operation
    from tit import constants as _const

    if analysis_type == "mask":
        from tit.opt.masks import validate_mask

        if coordinate_space.lower() != "mni":
            raise ValueError("Group mask analysis requires an MNI-space mask")
        validate_mask(mask_path)

    with track_operation(_const.TELEMETRY_OP_GROUP_ANALYSIS):
        out = _resolve_output_dir(output_dir)

        # File handler so group analysis logs are persisted
        timestamp = time.strftime("%Y%m%d_%H%M%S")
        log_file = out / f"group_analysis_{timestamp}.log"
        add_file_handler(log_file)

        logger.info(
            "Group analysis started: %d subjects, space=%s, type=%s",
            len(subject_ids),
            space,
            analysis_type,
        )

        dispatch: dict[str, callable] = {
            "spherical": lambda a: (
                a.analyze_spheres(
                    spheres=spheres,
                    coordinate_space=coordinate_space,
                    visualize=visualize,
                )
                if spheres
                else a.analyze_sphere(
                    center=center,
                    radius=radius,
                    coordinate_space=coordinate_space,
                    visualize=visualize,
                )
            ),
            "mask": lambda a: a.analyze_mask(
                mask_path=mask_path,
                coordinate_space=coordinate_space,
                visualize=visualize,
            ),
            "cortical": lambda a: a.analyze_cortex(
                atlas=atlas,
                region=region,
                visualize=visualize,
            ),
        }
        analyze_fn = dispatch[analysis_type]

        results: dict[str, AnalysisResult] = {}
        for sid in subject_ids:
            logger.info("Analyzing subject %s", sid)
            results[sid] = analyze_fn(
                Analyzer(sid, simulation, space, tissue_type, field=field)
            )

        df = _build_summary_df(results)
        csv_path = out / "group_summary.csv"
        df.to_csv(csv_path, index=False, float_format="%.3f")
        logger.info("Summary CSV written to %s", csv_path)

        plot_path = _generate_comparison_plot(df, out)
        logger.info("Comparison plot written to %s", plot_path)

        return GroupResult(
            subject_results=results,
            summary_csv_path=csv_path,
            comparison_plot_path=plot_path,
        )