fix to stats when no json file is defined
This commit is contained in:
@@ -2650,7 +2650,6 @@ def run_roi_second_level_analysis(
|
||||
correction_method: str | None = "fdr_bh",
|
||||
target_chroma: str = "hbo",
|
||||
graph_bounds: float | None = None,
|
||||
roi_config: str | Path | None = None,
|
||||
threshold_topo: bool = False,
|
||||
) -> DataFrame:
|
||||
|
||||
@@ -2805,35 +2804,13 @@ def run_roi_second_level_analysis(
|
||||
con_model_df = statsmodels_to_results(con_model, order=raw_picked.ch_names)
|
||||
|
||||
# --- DYNAMIC ROI PARSING ---
|
||||
roi_mapping = {}
|
||||
if roi_config is not None:
|
||||
raw_json = None
|
||||
if isinstance(roi_config, str) and os.path.exists(roi_config):
|
||||
with open(roi_config, 'r') as f:
|
||||
raw_json = json.load(f)
|
||||
elif isinstance(roi_config, dict):
|
||||
raw_json = roi_config
|
||||
|
||||
if raw_json:
|
||||
if "regions_of_interest" in raw_json:
|
||||
for roi_item in raw_json["regions_of_interest"]:
|
||||
roi_name = roi_item.get("name")
|
||||
channels = roi_item.get("channels", [])
|
||||
if roi_name and channels:
|
||||
roi_mapping[roi_name] = channels
|
||||
else:
|
||||
roi_mapping = raw_json
|
||||
|
||||
if roi_mapping:
|
||||
ch_to_roi = {}
|
||||
for roi_name, channels in roi_mapping.items():
|
||||
for ch in channels:
|
||||
ch_to_roi[ch] = roi_name
|
||||
ch_to_roi[ch.split()[0]] = roi_name
|
||||
|
||||
con_summary['ROI'] = con_summary[ch_col].apply(
|
||||
lambda x: ch_to_roi.get(x, ch_to_roi.get(x.split()[0], None) if isinstance(x, str) else None)
|
||||
)
|
||||
if 'ROI' not in con_summary.columns or con_summary['ROI'].dropna().empty:
|
||||
if df_roi_all is not None and 'ROI' in df_roi_all.columns and ch_col in df_roi_all.columns:
|
||||
# Create channel -> ROI mapping from df_roi_all
|
||||
ch_to_roi = df_roi_all.dropna(subset=['ROI', ch_col]).set_index(ch_col)['ROI'].to_dict()
|
||||
con_summary['ROI'] = con_summary[ch_col].apply(
|
||||
lambda x: ch_to_roi.get(x, ch_to_roi.get(x.split()[0], None) if isinstance(x, str) else None)
|
||||
)
|
||||
|
||||
unique_rois = []
|
||||
if 'ROI' in con_summary.columns:
|
||||
@@ -2925,7 +2902,7 @@ def run_cross_group_second_level_analysis(
|
||||
target_chroma: str = "hbo",
|
||||
selected_event: str | None = None,
|
||||
graph_bounds: tuple[float, float] | list[float] | None = None,
|
||||
roi_config: Path | str | None = None,
|
||||
roi_channel_maps: dict[str, dict[str, str]] | None = None,
|
||||
threshold_topo: bool = False,
|
||||
) -> DataFrame:
|
||||
|
||||
@@ -3129,21 +3106,13 @@ def run_cross_group_second_level_analysis(
|
||||
con_model_df = pd.DataFrame(contrast_data)
|
||||
|
||||
# --- DYNAMIC ROI PARSING ---
|
||||
roi_mapping = {}
|
||||
if roi_config is not None and os.path.exists(roi_config):
|
||||
with open(roi_config, 'r') as f:
|
||||
raw_json = json.load(f)
|
||||
if "regions_of_interest" in raw_json:
|
||||
for roi_item in raw_json["regions_of_interest"]:
|
||||
roi_mapping[roi_item.get("name")] = roi_item.get("channels", [])
|
||||
if roi_channel_maps:
|
||||
def _lookup_roi(row):
|
||||
m = roi_channel_maps.get(row['clean_ID'], {})
|
||||
ch = row[ch_col]
|
||||
return m.get(ch, m.get(ch.split()[0]) if isinstance(ch, str) else None)
|
||||
|
||||
if roi_mapping:
|
||||
ch_to_roi = {}
|
||||
for roi_name, channels in roi_mapping.items():
|
||||
for ch in channels:
|
||||
ch_to_roi[ch] = roi_name
|
||||
ch_to_roi[ch.split()[0]] = roi_name
|
||||
con_summary['ROI'] = con_summary[ch_col].apply(lambda x: ch_to_roi.get(x, ch_to_roi.get(x.split()[0], None) if isinstance(x, str) else None))
|
||||
con_summary['ROI'] = con_summary.apply(_lookup_roi, axis=1)
|
||||
|
||||
unique_rois = [r for r in con_summary['ROI'].dropna().unique() if r != ""] if 'ROI' in con_summary.columns else ['All_Channels']
|
||||
|
||||
@@ -3454,7 +3423,8 @@ def run_cross_group_contrast_analysis(
|
||||
df_contrasts_a: DataFrame,
|
||||
df_contrasts_b: DataFrame,
|
||||
contrast_name: str,
|
||||
roi_json_path: str | Path | None,
|
||||
roi_channel_maps_a: dict[str, dict[str, str]],
|
||||
roi_channel_maps_b: dict[str, dict[str, str]],
|
||||
group_a_name: str = "Group A",
|
||||
group_b_name: str = "Group B",
|
||||
target_chroma: str = "hbo",
|
||||
@@ -3547,8 +3517,8 @@ def run_cross_group_contrast_analysis(
|
||||
print(f"[ERROR] Contrast '{contrast_name}' not found anywhere in {group_b_name}'s data.")
|
||||
return DataFrame()
|
||||
|
||||
roi_a = aggregate_channel_contrasts_to_roi(df_a_filt, roi_json_path, weighted=weighted)
|
||||
roi_b = aggregate_channel_contrasts_to_roi(df_b_filt, roi_json_path, weighted=weighted)
|
||||
roi_a = aggregate_channel_contrasts_to_roi(df_a_filt, roi_channel_maps_a, weighted=weighted)
|
||||
roi_b = aggregate_channel_contrasts_to_roi(df_b_filt, roi_channel_maps_b, weighted=weighted)
|
||||
|
||||
roi_a = roi_a[roi_a['Chroma'] == target_chroma]
|
||||
roi_b = roi_b[roi_b['Chroma'] == target_chroma]
|
||||
@@ -3897,7 +3867,7 @@ def run_roi_paired_contrast_analysis(
|
||||
|
||||
def aggregate_channel_contrasts_to_roi(
|
||||
df_contrasts: DataFrame,
|
||||
roi_json_path: str | Path | None,
|
||||
roi_channel_maps: dict[str, dict[str, str]],
|
||||
weighted: bool = True
|
||||
) -> DataFrame:
|
||||
"""
|
||||
@@ -3924,11 +3894,14 @@ def aggregate_channel_contrasts_to_roi(
|
||||
['ch_name', 'effect', 'stat', 'Chroma', 'contrast_name', 'ID']
|
||||
`stat` must be the t-statistic (ContrastType == 't'), since standard
|
||||
error is recovered as effect / stat.
|
||||
roi_json_path : str
|
||||
Path to the same regions.json used elsewhere in the pipeline, with
|
||||
the structure: {"regions_of_interest": [{"name": ..., "channels": [...]}]}
|
||||
`channels` entries should be bare source-detector names (e.g. "S1_D1"),
|
||||
matching the convention already used for the GLM-level ROI loading.
|
||||
roi_channel_maps : dict[str, dict[str, str]]
|
||||
Per-subject channel-to-ROI mapping, keyed by subject ID (the same
|
||||
ID values used in df_contrasts['ID']), e.g.
|
||||
{"sub-01": {"S1_D1 hbo": "Left", "S1_D1 hbr": "Left", ...}, ...}.
|
||||
This is the actual mapping generate_roi_results used for that
|
||||
subject (whichever tier produced it — JSON, geometric split, or
|
||||
per-channel fallback) — not re-derived here, so ROI assignments
|
||||
stay consistent with df_ind_dict for the same subject.
|
||||
weighted : bool, default True
|
||||
If True, combine channels within an ROI using inverse-variance
|
||||
weighting (weight = 1 / se^2), matching MNE-NIRS's own default
|
||||
@@ -3948,35 +3921,18 @@ def aggregate_channel_contrasts_to_roi(
|
||||
if not all(col in df_contrasts.columns for col in required_cols):
|
||||
raise ValueError(f"Input contrast DataFrame must include: {required_cols}")
|
||||
|
||||
# --- Load ROI definitions and build a channel-base -> ROI lookup ---
|
||||
# Channel base names (e.g. "S1_D1") map to both hbo/hbr rows via the
|
||||
# ch_name column ("S1_D1 hbo" / "S1_D1 hbr"), so we key on the base name.
|
||||
with open(roi_json_path, 'r') as f:
|
||||
roi_data = json.load(f)
|
||||
|
||||
ch_base_to_roi = {}
|
||||
for region in roi_data.get("regions_of_interest", []):
|
||||
roi_name = region["name"]
|
||||
for ch_base in region["channels"]:
|
||||
if ch_base in ch_base_to_roi:
|
||||
logger.warning(
|
||||
f"Channel '{ch_base}' assigned to multiple ROIs "
|
||||
f"('{ch_base_to_roi[ch_base]}' and '{roi_name}') — "
|
||||
f"using '{roi_name}' (last one wins)."
|
||||
)
|
||||
ch_base_to_roi[ch_base] = roi_name
|
||||
|
||||
df = df_contrasts.copy()
|
||||
df['ch_base'] = df['ch_name'].str.split().str[0] # "S1_D1 hbo" -> "S1_D1"
|
||||
df['ROI'] = df['ch_base'].map(ch_base_to_roi)
|
||||
|
||||
n_unassigned = df['ROI'].isna().sum()
|
||||
if n_unassigned:
|
||||
logger.warning(
|
||||
f"{n_unassigned} channel-rows did not match any ROI in "
|
||||
f"'{roi_json_path}' and will be excluded."
|
||||
)
|
||||
df['ch_base'] = df['ch_name'].str.split().str[0]
|
||||
|
||||
def lookup(row):
|
||||
m = roi_channel_maps.get(row['ID'], {})
|
||||
return m.get(row['ch_name'], m.get(row['ch_base']))
|
||||
|
||||
df['ROI'] = df.apply(lookup, axis=1)
|
||||
df = df.dropna(subset=['ROI'])
|
||||
|
||||
if df.empty:
|
||||
raise ValueError("No channel contrasts matched any subject's ROI mapping.")
|
||||
|
||||
# Recover standard error from the t-statistic: t = effect / se -> se = effect / t
|
||||
with np.errstate(divide='ignore', invalid='ignore'):
|
||||
@@ -5029,6 +4985,12 @@ def generate_roi_results(raw_haemo, df_design_matrix, glm_est, file_path, json_l
|
||||
|
||||
subject_id = sub['ID'].iloc[0] if 'ID' in sub.columns else ''
|
||||
n_conditions = sub['Condition'].nunique()
|
||||
|
||||
roi_channel_map = {
|
||||
raw_haemo.ch_names[idx]: roi_name
|
||||
for roi_name, indices in rois_formatted.items()
|
||||
for idx in indices
|
||||
}
|
||||
|
||||
sns.set_theme(style="whitegrid")
|
||||
fig, ax = plt.subplots(figsize=(max(6, 1.5 * sub['ROI'].nunique()), 5))
|
||||
@@ -5048,7 +5010,7 @@ def generate_roi_results(raw_haemo, df_design_matrix, glm_est, file_path, json_l
|
||||
plt.tight_layout()
|
||||
plt.close(fig)
|
||||
|
||||
return df_roi, fig
|
||||
return df_roi, roi_channel_map, fig
|
||||
|
||||
|
||||
|
||||
@@ -5495,7 +5457,7 @@ def process_participant(file_path, file_start, progress_callback=None):
|
||||
step_start = lap(step_start, timings, "Step 25")
|
||||
|
||||
# Step 26: Generate Region of Interest Results
|
||||
df_roi, fig_roi = generate_roi_results(raw_haemo, df_design_matrix, glm_est, file_path, json_location=JSON_LOCATION)
|
||||
df_roi, roi_channel_map, fig_roi = generate_roi_results(raw_haemo, df_design_matrix, glm_est, file_path, json_location=JSON_LOCATION)
|
||||
_enqueue("Region of Interest", fig_roi, png_queue)
|
||||
if progress_callback: progress_callback(26)
|
||||
logger.info("26")
|
||||
@@ -5523,7 +5485,7 @@ def process_participant(file_path, file_start, progress_callback=None):
|
||||
logger.info(f" {name:<25} {elapsed:7.3f}s")
|
||||
|
||||
logger.info(f"Total processing time: {sum(timings.values()):.3f}s")
|
||||
return raw_haemo, epochs, df_cha, df_roi, df_design_matrix, config_dict, fig_bytes_dict, contrast_results_dict, True
|
||||
return raw_haemo, epochs, df_cha, df_roi, df_design_matrix, config_dict, fig_bytes_dict, contrast_results_dict, roi_channel_map, True
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user