JALE/jale/ale.py
2025-07-22 14:12:37 +02:00

396 lines
14 KiB
Python

import logging
from pathlib import Path
from jale.core.analyses.balanced_contrast import balanced_contrast
from jale.core.analyses.clustering import clustering
from jale.core.analyses.contrast import contrast
from jale.core.analyses.main_effect import main_effect
from jale.core.analyses.probabilistic import probabilistic_ale
from jale.core.analyses.roi import roi_ale
from jale.core.utils.compile_experiments import compile_experiments
from jale.core.utils.contribution import contribution
from jale.core.utils.input import (
check_for_exp_independence,
check_params,
determine_target_n,
load_config,
load_dataframes,
setup_contrast_data,
)
from jale.core.utils.logger import setup_logger
def run_ale(yaml_path=None):
# Load config and set up paths
config = load_config(yaml_path)
project_path = Path(yaml_path).parent
# Create a logs folder (common across all analysis types)
(project_path / "logs").mkdir(parents=True, exist_ok=True)
# Initialize the logger
logger = setup_logger(project_path)
logger.info("Logger initialized and project setup complete.")
params = config.get("parameters", {})
params = check_params(params)
clustering_params = config.get("clustering_parameters", {})
exp_all_df, tasks, analysis_df = load_dataframes(project_path, config)
# Main loop to process each row in the analysis dataframe
for row_idx in range(analysis_df.shape[0]):
# skip empty rows - indicate 2nd effect for contrast analysis
if not isinstance(analysis_df.iloc[row_idx, 0], str):
continue
if analysis_df.iloc[row_idx, 0] == "M":
run_main_effect(
analysis_df, row_idx, project_path, params, exp_all_df, tasks
)
elif analysis_df.iloc[row_idx, 0][0] == "P":
run_probabilistic_ale(
analysis_df, row_idx, project_path, params, exp_all_df, tasks
)
elif analysis_df.iloc[row_idx, 0] == "C":
run_contrast_analysis(
analysis_df, row_idx, project_path, params, exp_all_df, tasks
)
elif analysis_df.iloc[row_idx, 0][0] == "B":
run_balanced_contrast(
analysis_df, row_idx, project_path, params, exp_all_df, tasks
)
elif analysis_df.iloc[row_idx, 0] == "Cluster":
run_ma_clustering(
analysis_df, row_idx, project_path, clustering_params, exp_all_df, tasks
)
logger.info("Analysis completed.")
def run_main_effect(analysis_df, row_idx, project_path, params, exp_all_df, tasks):
"""
Run a main-effect analysis based on the analysis dataframe and experiment info.
Parameters
----------
analysis_df : pandas.DataFrame
DataFrame containing the analysis information, including meta-analysis names
and conditions for experiment selection.
row_idx : int
Index of the current row in the analysis dataframe.
project_path : str or Path
Path to the project directory where results are saved.
params : dict
Dictionary of parameters for analysis, including Monte Carlo iterations and
subsample size.
exp_all_df : pandas.DataFrame
DataFrame containing all available experimental data.
tasks : pandas.DataFrame
DataFrame containing task information used for compiling experiments.
Returns
-------
None
The function performs computations and saves the results.
"""
logger = logging.getLogger("ale_logger")
meta_name = analysis_df.iloc[row_idx, 1]
result_path = project_path / f"Results/MainEffect/Volumes/{meta_name}_cFWE.nii"
if result_path.exists():
logger.info(f"Main Effect results for {meta_name} already exist.")
return
logger.info("Running Main-Effect Analysis")
conditions = analysis_df.iloc[row_idx, 2:].dropna().to_list()
exp_idxs, masks, mask_names = compile_experiments(conditions, tasks)
exp_df = exp_all_df.loc[exp_idxs].reset_index(drop=True)
check_for_exp_independence(exp_df)
main_effect(
project_path,
exp_df,
meta_name,
tfce_enabled=params["tfce_enabled"],
cutoff_predict_enabled=params["cutoff_predict_enabled"],
gm_masking=params["gm_masking"],
bin_steps=params["bin_steps"],
cluster_forming_threshold=params["cluster_forming_threshold"],
monte_carlo_iterations=params["monte_carlo_iterations"],
nprocesses=params["nprocesses"],
)
contribution(
project_path, exp_df, exp_idxs, meta_name, tasks, params["tfce_enabled"]
)
if masks:
for idx, mask in enumerate(masks):
roi_ale(
project_path,
exp_df,
meta_name,
mask,
mask_names[idx],
monte_carlo_iterations=params["monte_carlo_iterations"],
)
def run_probabilistic_ale(
analysis_df, row_idx, project_path, params, exp_all_df, tasks
):
"""
Run a probabilistic Activation Likelihood Estimation (ALE) analysis.
Parameters
----------
analysis_df : pandas.DataFrame
DataFrame containing the analysis information, including meta-analysis
names and conditions for experiment selection.
row_idx : int
Index of the current row in the analysis dataframe.
project_path : str or Path
Path to the project directory where results are saved.
params : dict
Dictionary of parameters for analysis, including Monte Carlo iterations
and subsample size.
exp_all_df : pandas.DataFrame
DataFrame containing all available experimental data.
tasks : pandas.DataFrame
DataFrame containing task information used for compiling experiments.
Returns
-------
None
The function performs computations and saves the results.
"""
logger = logging.getLogger("ale_logger")
meta_name = analysis_df.iloc[row_idx, 1]
target_n = (
int(analysis_df.iloc[row_idx, 0][1:])
if len(analysis_df.iloc[row_idx, 0]) > 1
else None
)
result_path = (
project_path
/ f"Results/Probabilistic/Volumes/{meta_name}_sub_ale_{target_n}.nii"
)
if result_path.exists():
logger.info(f"Probabilistic ALE results for {meta_name} already exist.")
return
logger.info("Running Probabilistic ALE")
conditions = analysis_df.iloc[row_idx, 2:].dropna().to_list()
exp_idxs, _, _ = compile_experiments(conditions, tasks)
exp_df = exp_all_df.loc[exp_idxs].reset_index(drop=True)
check_for_exp_independence(exp_df)
if target_n:
probabilistic_ale(
project_path,
exp_df,
meta_name,
target_n=target_n,
monte_carlo_iterations=params["monte_carlo_iterations"],
sample_n=params["subsample_n"],
nprocesses=params["nprocesses"],
)
else:
logger.warning(f"{meta_name}: Need to specify subsampling N")
def run_contrast_analysis(
analysis_df, row_idx, project_path, params, exp_all_df, tasks
):
"""
Run a contrast analysis between two meta-analyses.
Parameters
----------
analysis_df : pandas.DataFrame
DataFrame containing the analysis information.
row_idx : int
Index of the current row in the DataFrame.
project_path : str or Path
Path to the project directory.
params : dict
Dictionary of parameters for analysis, including significance threshold and
number of permutations.
exp_all_df : pandas.DataFrame
DataFrame containing all experiment data.
tasks : pandas.DataFrame
DataFrame containing task information.
Returns
-------
None
The function performs computations and saves the results as NIfTI files in the
specified `project_path` directory.
"""
meta_names, exp_dfs, exp_idxs = setup_contrast_data(
analysis_df, row_idx, exp_all_df, tasks
)
check_for_exp_independence(exp_dfs[0])
check_for_exp_independence(exp_dfs[1])
for idx, meta_name in enumerate(meta_names):
result_path = project_path / f"Results/MainEffect/Volumes/{meta_name}_cFWE.nii"
if not result_path.exists():
logger = logging.getLogger("ale_logger")
logger.info(
f"Running main effect for {meta_name} as prerequisite for contrast analysis"
)
main_effect(
project_path,
exp_dfs[idx],
meta_name,
tfce_enabled=params["tfce_enabled"],
cutoff_predict_enabled=params["cutoff_predict_enabled"],
bin_steps=params["bin_steps"],
cluster_forming_threshold=params["cluster_forming_threshold"],
monte_carlo_iterations=params["monte_carlo_iterations"],
nprocesses=params["nprocesses"],
)
contribution(
project_path,
exp_dfs[idx],
exp_idxs[idx],
meta_name,
tasks,
params["tfce_enabled"],
)
exp_overlap = set(exp_dfs[0].index) & set(exp_dfs[1].index)
exp_dfs = [exp_dfs[0].drop(exp_overlap), exp_dfs[1].drop(exp_overlap)]
contrast(
project_path,
meta_names,
correction_method=params["contrast_correction_method"],
significance_threshold=params["significance_threshold"],
null_repeats=params["contrast_permutations"],
nprocesses=params["nprocesses"],
)
def run_balanced_contrast(
analysis_df, row_idx, project_path, params, exp_all_df, tasks
):
"""
Run a balanced contrast analysis using the provided experiment data.
Parameters
----------
analysis_df : pandas.DataFrame
DataFrame containing the analysis information.
row_idx : int
Index of the current row in the DataFrame.
project_path : str or Path
Path to the project directory.
params : dict
Dictionary of parameters for analysis, including TFCE and cutoff prediction settings.
exp_all_df : pandas.DataFrame
DataFrame containing all experiment data.
tasks : pandas.DataFrame
DataFrame containing task information.
Returns
-------
None
The function performs computations and saves the results as NIfTI files in the
specified `project_path` directory.
"""
meta_names, exp_dfs, exp_idxs = setup_contrast_data(
analysis_df, row_idx, exp_all_df, tasks
)
check_for_exp_independence(exp_dfs[0])
check_for_exp_independence(exp_dfs[1])
target_n = determine_target_n(analysis_df.iloc[row_idx, 0], exp_dfs)
# Check if subsampling ALE were already run; if not - run them
for idx, meta_name in enumerate(meta_names):
result_path = (
project_path
/ f"Results/MainEffect/Volumes/{meta_name}_sub_ale_{target_n}.nii"
)
if not result_path.exists():
logger = logging.getLogger("ale_logger")
logger.info(
f"Running subsampling ale for {meta_name} as prerequisite for balanced contrast analysis"
)
probabilistic_ale(
project_path,
exp_dfs[idx],
meta_name,
target_n=target_n,
monte_carlo_iterations=params["monte_carlo_iterations"],
sample_n=params["subsample_n"],
nprocesses=params["nprocesses"],
)
balanced_contrast(
project_path,
exp_dfs,
meta_names,
target_n,
difference_iterations=params["difference_iterations"],
monte_carlo_iterations=params["monte_carlo_iterations"],
nprocesses=params["nprocesses"],
)
def run_ma_clustering(analysis_df, row_idx, project_path, params, exp_all_df, tasks):
"""
Run a meta-analysis clustering (MA-Clustering) analysis based on the analysis dataframe and experiment info.
Parameters
----------
analysis_df : pandas.DataFrame
DataFrame containing the analysis information, including meta-analysis names and conditions for experiment selection.
row_idx : int
Index of the current row in the analysis dataframe.
project_path : str or Path
Path to the project directory where results are saved.
params : dict
Dictionary of parameters for analysis, including clustering method, correlation type, linkage method, max clusters, subsample fraction, sampling iterations and null iterations.
exp_all_df : pandas.DataFrame
DataFrame containing all available experimental data.
tasks : pandas.DataFrame
DataFrame containing task information used for compiling experiments.
Returns
-------
None
The function performs computations and saves the results.
"""
logger = logging.getLogger("ale_logger")
logger.info("Running MA Clustering")
meta_name = analysis_df.iloc[row_idx, 1]
conditions = analysis_df.iloc[row_idx, 2:].dropna().to_list()
exp_idxs, masks, mask_names = compile_experiments(conditions, tasks)
exp_df = exp_all_df.loc[exp_idxs].reset_index(drop=True)
check_for_exp_independence(exp_df)
logger.info(
f"{meta_name} : {len(exp_idxs)} experiments; average of {exp_df.Subjects.mean():.2f} subjects per experiment"
)
clustering(
project_path=project_path,
exp_df=exp_df,
meta_name=meta_name,
correlation_type=params["correlation_type"],
clustering_method=params["clustering_method"],
linkage_method=params["linkage_method"],
max_clusters=params["max_clusters"],
subsample_fraction=params["subsample_fraction"],
sampling_iterations=params["sampling_iterations"],
null_iterations=params["null_iterations"],
use_pooled_std=params["use_pooled_std"],
)