unhardcoding and general cleanup

This commit is contained in:
2026-07-17 00:07:56 -07:00
parent 2ff7cda93a
commit 0680718398
7 changed files with 1614 additions and 555 deletions
+147 -99
View File
@@ -17,63 +17,111 @@ from src.shared.shareddata import APP_NAME
PARAMETERIZED_INDEXES = {
0: [
{
"key": "info",
"label": "Tests whether one ROI's response during one condition reliably differs from zero across subjects.\nIf significant, you can claim: This region's signal during this condition is consistently non-zero across your sample - not just noise.\nIt does NOT say: Whether that response is localized/specific to this region, or whether it reflects real neural activity versus systemic physiology (blood pressure, arousal) shared across the whole head during any active task.",
"default": "Okay.",
"type": str,
},
{
"key": "p_value",
"key": "p_threshold",
"label": "Significance threshold P-value (e.g. 0.05)",
"default": "0.05",
"type": float,
},
{
"key": "min_subjects",
"label": "Minimum number of participants to process",
"default": "5",
"type": int,
},
{
"key": "correction_method",
"label": "Correction method to utilize. Valid values are 'fdr_bh', 'None'",
"default": "fdr_bh",
"type": str,
},
{
"key": "target_chroma",
"label": "Which chroma to target. Valid values are 'hbo', 'hbr'",
"default": "hbo",
"type": str,
},
{
"key": "graph_bounds",
"label": "Graph Y-Limit (Optional, e.g. 1e-5)",
"default": "0.0", # Set to 0.0 to auto-scale
"label": "Graph Upper/Lower Limit",
"default": "0.0",
"type": float,
}
],
1: [
{
"key": "info",
"label": "For one condition, subtracts each subject's ROI_A response from their ROI_B response, then tests whether that per-subject difference is reliably non-zero.\nIf significant, you can claim: The two regions respond differently from each other during this specific condition - a real spatial contrast, since shared systemic noise partially cancels in the subtraction.\nIt does NOT say: Anything about whether the condition itself produced meaningful activity at all (only a relative difference between two places); and its power depends on the two ROIs' noise being correlated across subjects, which isn't guaranteed.",
"default": "Okay.",
"key": "p_threshold",
"label": "Significance threshold P-value (e.g. 0.05)",
"default": "0.05",
"type": float,
},
{
"key": "min_subjects",
"label": "Minimum number of participants to process",
"default": "5",
"type": int,
},
{
"key": "correction_method",
"label": "Correction method to utilize. Valid values are 'fdr_bh', 'None'",
"default": "None",
"type": str,
},
{
"key": "target_chroma",
"label": "Which chroma to target. Valid values are 'hbo', 'hbr'",
"default": "hbo",
"type": str,
},
{
"key": "roi_a",
"label": "ROI A (e.g. contralateral region name from regions.json)",
"default": "",
"type": str,
"default": [],
"type": list,
},
{
"key": "roi_b",
"label": "ROI B (e.g. ipsilateral region name from regions.json)",
"default": "",
"type": str,
},
{
"key": "p_value",
"label": "Significance threshold P-value (e.g. 0.05)",
"default": "0.05",
"type": float,
},
"default": [],
"type": list,
}
],
2: [
{
"key": "info",
"label": "Uses a contrast fit jointly within each subject's GLM (Condition A minus Condition B, estimated together), then aggregates that per-channel contrast to ROI level and tests it against zero across subjects.\nIf significant, you can claim: The two conditions produce reliably different responses at this ROI, with systemic noise largely cancelled at the model-fitting stage itself - the most statistically efficient of the three.\nIt does NOT say: Which region the difference is localized to, unless you compare the sign/pattern across multiple ROIs",
"default": "Okay.",
"type": str,
},
{
"key": "p_value",
"label": "Significance threshold P-value (e.g. 0.05)",
"default": "0.05",
"type": float,
},
{
"key": "min_subjects",
"label": "Minimum number of participants to process",
"default": "5",
"type": int,
},
{
"key": "correction_method",
"label": "Correction method to utilize. Valid values are 'fdr_bh', 'None'",
"default": "fdr_bh",
"type": str,
},
{
"key": "target_chroma",
"label": "Which chroma to target. Valid values are 'hbo', 'hbr'",
"default": "hbo",
"type": str,
},
{
"key": "contrast_name",
"label": "Name of the contrast to use",
"default": [],
"type": list,
},
{
"key": "weighted",
"label": "Use inverse-variance weighting to minimize noisy channels",
"default": True,
"type": bool,
},
{
"key": "graph_bounds",
"label": "Graph Upper/Lower Limit",
@@ -84,9 +132,23 @@ PARAMETERIZED_INDEXES = {
}
DESCRIPTION = """0. ROI vs. Zero (run_roi_second_level_analysis)
\nTests whether one ROI's response during one condition reliably differs from zero across subjects - a one-sample t-test on each subject's ROI-averaged theta. A significant result means the region's signal during this condition is consistently non-zero across your sample, not just noise. It does not tell you whether the response is localized/specific to this region, or whether it reflects real neural activity versus systemic physiology (blood pressure, arousal) shared across the whole head during any active task - a single-condition "vs. zero" test can't distinguish those two explanations on its own.
\nIf you expected significance here and didn't get it, likely causes include: the sample size is simply small relative to between-subject variability in true response magnitude or HRF shape (individual differences in timing/amplitude inflate the variance a t-test divides by); the ROI's channel composition differs slightly across subjects (missing channels get down-weighted or excluded from the inverse-variance average, diluting a real signal); FDR correction across many ROIs is suppressing a modest true effect that would clear an uncorrected threshold; or the condition itself may not reliably engage this region the way you assumed (worth checking the single-subject/individual-level results for this ROI before concluding the group effect isn't there).
\n\n1. Paired ROI Contrast (run_roi_paired_contrast_analysis)
\nFor one condition, subtracts each subject's ROI_A response from their ROI_B response, then tests whether that per-subject difference is reliably non-zero. A significant result is a genuine spatial contrast - the two regions respond differently from each other during this specific condition, with shared systemic noise partially cancelling in the subtraction. It says nothing about whether the condition produced meaningful activity at all (only a relative difference between two places), and its power depends entirely on ROI_A and ROI_B varying together across subjects - an assumption that isn't guaranteed.
\nIf this test underperforms a plain ROI-vs-zero result, which can occur, the most likely explanation is that ROI_A and ROI_B's noise isn't well-correlated across your subjects. The math is variance(A - B) = variance(A) + variance(B) - 2·covariance(A,B): subtraction only helps when the shared/systemic component is large relative to independent noise in each region. If the two regions are picking up largely independent noise sources (motion artifact affecting one side more, different channel quality, etc.), subtracting adds variance rather than removing it, and can turn a detectable single-ROI effect into an underpowered paired one. Small sample size makes this worse, since the covariance itself is poorly estimated with few subjects.
\n\n2. Joint Contrast, ROI-Aggregated (aggregate_channel_contrasts_to_roi + one-sample test)
\nUses a contrast fit jointly within each subject's GLM (Condition A minus Condition B, estimated together), then aggregates that per-channel contrast to ROI level using inverse-variance weighting, and tests it against zero across subjects. A significant result means the two conditions produce reliably different responses at this ROI, with systemic noise largely cancelled at the model-fitting stage itself - the most statistically efficient of the three within-group methods, since the correlation between conditions is handled natively rather than inferred afterward. It does not tell you where the difference is localized on its own - for that, compare the sign/pattern across multiple ROIs: opposite signs across regions indicates a real, spatially-specific effect, while the same sign everywhere suggests diffuse/systemic noise rather than localized activity (as seen when comparing a real task-vs-task contrast against a task-vs-inert-marker contrast).
\nIf this comes back non-significant despite expecting an effect, first check whether the two conditions are actually similar enough in their neural engagement of this ROI that a small or genuinely near-zero contrast is the correct answer - not every ROI should differentiate every pair of tasks, and a null result here can be the right result. Beyond that: FDR correction across every ROI in your regions file can suppress a real but modest contrast; the inverse-variance weighting can be destabilized if a few channels within the ROI have very noisy or near-zero t-statistics (their standard error estimate becomes huge or unstable); and - as always - small subject counts limit the achievable degrees of freedom regardless of how clean the underlying per-channel estimates are.
\n\n
\nWhy channels needed to be aggregated into ROIs: Testing every channel independently means paying a steep multiple-comparisons tax - with dozens of channels, FDR/Bonferroni correction demands very large effect sizes to call anything significant, and at small subject counts (n=5) essentially nothing survives even when a real, consistent effect exists. Collapsing channels into a handful of anatomically meaningful ROIs cuts the number of independent tests from a minimum of ~40 down to 2-8, which lets a genuinely present effect actually clear correction. It also matches the scientific question better: you have a hypothesis about regions (contralateral motor cortex, prefrontal cortex), not about individual source-detector pairs, so testing at the ROI level is testing the thing you actually believe in, using inverse-variance weighting so noisier channels contribute less to the region's combined estimate rather than diluting it equally.
\nWhy some analyses needed contrasts instead of raw values: A single condition's GLM beta is only ever measured relative to the model's implicit intercept, and that intercept absorbs whatever's happening for the rest of the recording - including systemic physiology (blood pressure, arousal, general vascular reactivity) that rises during almost any active task, not just the one you care about. Testing a raw "vs. zero" value can't tell a real, localized neural response apart from that shared full-head noise. A contrast - either a within-subject spatial subtraction (ROI A minus ROI B) or a jointly-fit task contrast (Condition A minus Condition B, estimated together in one GLM) cancels out whatever's common to both halves of the subtraction, leaving something closer to the actual differential signal.
\nWhy a minimum subject count is enforced: Every one of these tests is a t-test, and a t-test's ability to detect a real effect (its power) depends heavily on degrees of freedom - at n=5 (df=4), even a fairly large true effect can produce a middling p-value, and at n=2 (df=1) the test is barely meaningful at all regardless of the underlying data. The min_subjects floor exists to stop a channel or ROI from being silently tested (and potentially reported as significant or non-significant) on a sample too small for the resulting p-value to mean anything reliable - it's better to explicitly skip and flag an underpowered channel than to quietly produce a number that looks statistically legitimate but isn't backed by enough independent observations to trust."""
class InterGroupStatsWidget(InterGroupUIMixin, FlaresBaseWidget):
def __init__(self, haemo_dict, cha, df_ind, design_matrix, contrast_results, group):
def __init__(self, haemo_dict, cha, df_ind, design_matrix, contrast_results, group, json_location):
super().__init__("InterGroupStats")
self.setWindowTitle(f"Inter-Group Stats Viewer - {APP_NAME.upper()}")
self.haemo_dict = haemo_dict
@@ -95,12 +157,12 @@ class InterGroupStatsWidget(InterGroupUIMixin, FlaresBaseWidget):
self.design_matrix = design_matrix
self.contrast_results = contrast_results
self.group = group
self.json_location = json_location
self.setup_inter_group_ui(["0 (Significance)", "1 (More significasd)", "2 (moreeee)"])
self.setup_inter_group_ui(["0 (ROI vs. Zero)", "1 (Paired ROI Contrast)", "2 (Joint Contrast, ROI-Aggregated)"], placeholder_text=DESCRIPTION)
def process_request(self):
request = self.get_common_request_data(PARAMETERIZED_INDEXES)
request = self.get_common_request_data(PARAMETERIZED_INDEXES, self.json_location, self.contrast_results)
if request is None:
return
@@ -137,9 +199,15 @@ class InterGroupStatsWidget(InterGroupUIMixin, FlaresBaseWidget):
for idx in selected_indexes:
if idx == 0:
params = param_values.get(idx, {})
p_val = params.get("p_value", 0.05)
p_threshold = params.get("p_threshold", 0.05)
min_subjects = params.get("min_subjects", 5)
correction_method = params.get("correction_method", "fdr_bh")
target_chroma = params.get("target_chroma", "hbo")
graph_bounds = params.get("graph_bounds", 0.0)
if correction_method == "None":
correction_method = None
if df_group.empty:
print("No ROI data (df_ind) found for selected participants.")
continue
@@ -165,90 +233,77 @@ class InterGroupStatsWidget(InterGroupUIMixin, FlaresBaseWidget):
else:
all_cha_filtered = all_cha
# ---------------------------------------------------------------------
# run_roi_second_level_analysis
#
# Tests: is this ROI's activation reliably different from zero, for one
# condition, across subjects? (One-sample t-test per ROI.)
#
# CAUTION: "vs zero" includes systemic/global physiology shared across
# the whole head (blood pressure, arousal, etc.), not just localized
# neural response — a significant result here doesn't by itself prove
# the effect is spatially specific to this ROI.
# ---------------------------------------------------------------------
run_roi_second_level_analysis(
df_roi_all=df_filtered,
df_cha_all=all_cha_filtered,
raw_haemo=p_haemo,
p_threshold=p_val,
min_subjects=len(selected_file_paths),
correction_method='fdr_bh',
target_chroma='hbo',
p_threshold=p_threshold,
min_subjects=min_subjects,
correction_method=correction_method,
target_chroma=target_chroma,
graph_bounds=graph_bounds if graph_bounds > 0.0 else None,
roi_config=r"C:\Users\tyler\Desktop\research\flares\regions.json"
roi_config=self.json_location
)
elif idx == 1:
params = param_values.get(idx, {})
p_threshold = params.get("p_threshold", 0.05)
min_subjects = params.get("min_subjects", 5)
correction_method = params.get("correction_method", "None")
target_chroma = params.get("target_chroma", "hbo")
roi_a = params.get("roi_a", "").strip()
roi_b = params.get("roi_b", "").strip()
if not selected_event:
print("Paired ROI contrast requires a specific event/condition "
"to be selected — pick one from the Event dropdown first.")
"to be selected - pick one from the Event dropdown first.")
continue
if df_group.empty:
print("No ROI data (df_ind) found for selected participants.")
continue
params = param_values.get(idx, {})
roi_a = params.get("roi_a", "").strip()
roi_b = params.get("roi_b", "").strip()
p_val = params.get("p_value", 0.05)
if correction_method == "None":
correction_method = None
if not roi_a or not roi_b:
print("Both ROI A and ROI B must be specified.")
continue
# ---------------------------------------------------------------------
# run_roi_paired_contrast_analysis
#
# Tests: within one condition, does ROI_A's activation differ from
# ROI_B's, per subject? (Paired one-sample t-test on the per-subject
# difference, e.g. Right_PFC minus Left_PFC for a laterality check.)
#
# Only gains power over testing ROI_A and ROI_B separately if the two
# ROIs' noise is correlated across subjects (shared systemic component
# cancels in the subtraction). If they vary independently, this test
# can be WEAKER than testing either ROI alone — check per-subject
# correlation between ROI_A and ROI_B if this test underperforms.
run_roi_paired_contrast_analysis(
df_roi_all=df_group, # unfiltered — function filters internally
df_roi_all=df_group,
roi_pairs=(roi_a, roi_b),
condition=selected_event,
target_chroma='hbo',
min_subjects=min(5, len(selected_file_paths)),
p_threshold=p_val,
correction_method=None, # single pre-specified contrast
target_chroma=target_chroma,
min_subjects=min_subjects,
p_threshold=p_threshold,
correction_method=correction_method,
roi_a_label=roi_a,
roi_b_label=roi_b,
)
elif idx == 2:
params = param_values.get(idx, {})
p_threshold = params.get("p_threshold", 0.05)
min_subjects = params.get("min_subjects", 5)
correction_method = params.get("correction_method", "fdr_bh")
target_chroma = params.get("target_chroma", "hbo")
contrast_name = params.get("contrast_name", "2.0_vs_3.0")
weighted = params.get("weighted", True)
graph_bounds = params.get("graph_bounds", 0.0)
if not selected_event:
print("Joint contrast ROI analysis requires a specific contrast "
"to be selected from the Event dropdown first.")
continue
# Build the channel-level contrast dataframe for selected
# participants + selected contrast, same pattern used in
# GroupViewerWidget.show_brain_images.
contrast_name = "15.0_vs_2.0" # <-- change this to test other contrasts
print(f"[TEMP HARDCODE] Using contrast '{contrast_name}' "
f"instead of dropdown selection ('{selected_event}') for option 2.")
# Build the channel-level contrast dataframe for selected
# participants + selected contrast, same pattern used in
# GroupViewerWidget.show_brain_images.
if not contrast_name:
print("Contrast name must be specified.")
continue
all_contrasts = []
for fp in selected_file_paths:
condition_dfs = self.contrast_results.get(fp)
@@ -258,10 +313,6 @@ class InterGroupStatsWidget(InterGroupUIMixin, FlaresBaseWidget):
if contrast_name in condition_dfs:
df = condition_dfs[contrast_name].copy()
df["ID"] = fp
# contrast_results dict values don't carry a
# contrast_name column themselves — that's only
# stamped on during CSV export. Add it here since
# aggregate_channel_contrasts_to_roi requires it.
df["contrast_name"] = contrast_name
all_contrasts.append(df)
else:
@@ -275,16 +326,13 @@ class InterGroupStatsWidget(InterGroupUIMixin, FlaresBaseWidget):
df_contrasts = pd.concat(all_contrasts, ignore_index=True)
params = param_values.get(idx, {})
p_val = params.get("p_value", 0.05)
graph_bounds = params.get("graph_bounds", 0.0)
try:
roi_theta = aggregate_channel_contrasts_to_roi(
df_contrasts,
roi_json_path=r"C:\Users\tyler\Desktop\research\flares\regions.json",
weighted=True,
roi_json_path=self.json_location,
weighted=weighted,
)
except Exception as e:
print(f"Failed to aggregate contrasts to ROI: {e}")
continue
@@ -294,22 +342,22 @@ class InterGroupStatsWidget(InterGroupUIMixin, FlaresBaseWidget):
"(check regions.json channel names against this montage).")
continue
# TODO: Come back to this
# df_cha_all intentionally omitted (None): the topography
# section of run_roi_second_level_analysis expects
# single-condition Condition values in df_cha_all, which
# doesn't semantically match a contrast name — skip it here
# doesn't semantically match a contrast name - skip it here
# rather than pass mismatched data.
run_roi_second_level_analysis(
df_roi_all=roi_theta,
df_cha_all=None,
raw_haemo=p_haemo,
p_threshold=p_val,
min_subjects=min(5, len(selected_file_paths)),
correction_method='fdr_bh',
target_chroma='hbo',
p_threshold=p_threshold,
min_subjects=min_subjects,
correction_method=correction_method,
target_chroma=target_chroma,
graph_bounds=graph_bounds if graph_bounds > 0.0 else None,
)
else:
print(f"No method defined for index {idx}")