Two sample permutation test of respiratory phase angles at the group level#

This notebook runs permutation tests to compare two samples of respiratory phase angles. This is useful when you want to compare the phase angles of two different conditions or groups.

[2]:
import pickle as pkl
import numpy as np
from pathlib import Path

from pyriodic.preproc import RawSignal
from pyriodic.permutation import permutation_test_between_samples
from pyriodic.phase_events import create_phase_events

/Users/au661930/Library/CloudStorage/OneDrive-Aarhusuniversitet/Dokumenter/projects/_BehaviouralBreathing/code/AnalysisBreathingBehaviour/BreathingBehaviourVenv/lib/python3.13/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm
[3]:
# preprocess the data and extract phase angles for each subject for correct and incorrect trials
data_path = Path("../../data/raw/intermediate")
circs = {}
for file_path in data_path.iterdir():
    if file_path.suffix != ".pkl":
        continue
    subj_id = file_path.name.split("_")[1]
    with open(file_path, 'rb') as f:
            data = pkl.load(f)


    behav_data = data["behav_data"]
    resp_ts, sfreq, event_samples, event_ids, labels, correct = data["raw"], data["sfreq"], behav_data["event_samples"], behav_data["event_ids"], behav_data["event_type"], behav_data["correct"]

    start_sample = event_samples[0] - 1000  # start 1000 samples before the first event

    # subtract start sample from events (the first column)
    event_samples -= start_sample


    # initialise RawSignal object
    raw = RawSignal(resp_ts, fs=sfreq)
    raw.filter_bandpass(low=0.1, high=1.0)
    raw.smoothing(window_size=500)
    raw.zscore()

    PA, peaks, troughs = raw.phase_twopoint(prominence=0.1)
    correct_event_samples = [
        samp for samp, label, corr in zip(event_samples, labels, correct) if "target" in label and corr == 1.0
    ]

    correct_targets = create_phase_events(
        PA, events=correct_event_samples
    )

    incorrect_event_samples = [
        samp for samp, label, corr in zip(event_samples, labels, correct) if "target" in label and corr == 0.0
    ]
    incorrect_targets = create_phase_events(
        PA, events=incorrect_event_samples
    )

    circs[subj_id] = {
        "correct": correct_targets,
        "incorrect": incorrect_targets
    }
Rejected 0 out of 273 events (0.0%)
Rejected 0 out of 54 events (0.0%)
Rejected 0 out of 305 events (0.0%)
Rejected 0 out of 73 events (0.0%)
Rejected 0 out of 320 events (0.0%)
Rejected 0 out of 70 events (0.0%)
Rejected 0 out of 297 events (0.0%)
Rejected 0 out of 67 events (0.0%)
Rejected 0 out of 303 events (0.0%)
Rejected 0 out of 71 events (0.0%)
Rejected 0 out of 226 events (0.0%)
Rejected 0 out of 77 events (0.0%)
Rejected 0 out of 242 events (0.0%)
Rejected 0 out of 87 events (0.0%)
Rejected 0 out of 267 events (0.0%)
Rejected 0 out of 88 events (0.0%)
Rejected 0 out of 261 events (0.0%)
Rejected 0 out of 114 events (0.0%)
Rejected 0 out of 212 events (0.0%)
Rejected 0 out of 38 events (0.0%)
Rejected 0 out of 243 events (0.0%)
Rejected 0 out of 79 events (0.0%)
Rejected 0 out of 312 events (0.0%)
Rejected 0 out of 56 events (0.0%)
Rejected 0 out of 293 events (0.0%)
Rejected 0 out of 71 events (0.0%)
Rejected 0 out of 253 events (0.0%)
Rejected 0 out of 78 events (0.0%)
Rejected 0 out of 314 events (0.0%)
Rejected 0 out of 70 events (0.0%)
Rejected 0 out of 316 events (0.0%)
Rejected 0 out of 44 events (0.0%)
Rejected 0 out of 332 events (0.0%)
Rejected 0 out of 66 events (0.0%)
Rejected 0 out of 282 events (0.0%)
Rejected 0 out of 74 events (0.0%)
Rejected 0 out of 312 events (0.0%)
Rejected 0 out of 70 events (0.0%)
Rejected 0 out of 246 events (0.0%)
Rejected 0 out of 113 events (0.0%)
Rejected 0 out of 278 events (0.0%)
Rejected 0 out of 71 events (0.0%)
Rejected 0 out of 294 events (0.0%)
Rejected 0 out of 95 events (0.0%)
Rejected 0 out of 308 events (0.0%)
Rejected 0 out of 62 events (0.0%)
Rejected 0 out of 327 events (0.0%)
Rejected 0 out of 66 events (0.0%)
Rejected 0 out of 330 events (0.0%)
Rejected 0 out of 58 events (0.0%)
Rejected 0 out of 257 events (0.0%)
Rejected 0 out of 100 events (0.0%)
[5]:
n_permutations = 1000

group_observed_stats = []
group_null_stats = []
group_pvals = []


for subj_id in circs:

    correct_targets_tmp = circs[subj_id]["correct"]
    incorrect_targets_tmp = circs[subj_id]["incorrect"]

    # Run subject-level permutation test
    obs, pval, null_distribution = permutation_test_between_samples(
        incorrect_targets_tmp.data,
        correct_targets_tmp.data,
        n_permutations=n_permutations,
        verbose=False,
        return_null_distribution=True,
    )

    group_observed_stats.append(obs)
    group_null_stats.append(null_distribution)
    group_pvals.append(pval)


# Group-level observed statistic: average of subject-level observed stats
# On average, do subjects show evidence of difference in phase for irregular and regular targets?
group_obs = np.mean(group_observed_stats)

# Group-level null: average of null stats across subjects, per null_stat
# Reference distribution of what kind of group-level effect you’d expect if there was no difference between irregular and regular targets
group_null = [
    np.mean([nulls[i] for nulls in group_null_stats])
    for i in range(n_permutations)
]

# Group-level p-value
p_val = (np.sum(np.array(group_null) >= group_obs) + 1) / (len(group_null) + 1)

print(f"Group-level statistic = {group_obs:.3f}, p = {p_val:.4f}")

Group-level statistic = 0.078, p = 0.2827