0todd0000 / spm1d

One-Dimensional Statistical Parametric Mapping in Python
GNU General Public License v3.0
60 stars 21 forks source link

Results of (non) parametric paired t-test #275

Closed BeFaer closed 2 months ago

BeFaer commented 6 months ago

Hello everyone,

I am currently trying to conduct a paired samples t-test on a set of data containing information on the movement of several joints over time. My goal is to compare two different motion capture systems and identify time periods, where these show statistically significant differences.

The data I am trying to run the SPM operations on contains the "overall grand mean" of all motion capture data for the given joint degree of freedom, averaged across all participants and their respective trials. I have time-normalized the data, however not to 101 samples as is shown in several explanatory YouTube videos and the example datasets as that lead to great alterations in the waveforms of both systems. Currently I am working with 320 data entries per movement for both systems I used.

My code currently looks like this:

# transpose overall_grand_mean dataframes so they work with SPM1D
# also convert them to np arrays
vic_spm_data = vic_overall_grand_mean.transpose()
vic_spm_data = vic_spm_data.to_numpy()
oc_spm_data = oc_overall_grand_mean.transpose()
oc_spm_data = oc_spm_data.to_numpy().astype(float)

# Rows of interest
rows_of_interest = [
    'hip_flexion_r', 'hip_adduction_r', 'hip_rotation_r',
    'hip_flexion_l', 'hip_adduction_l', 'hip_rotation_l',
    'knee_angle_r', 'knee_adduction_r', 'knee_angle_l', 'knee_adduction_l',
    'ankle_angle_r', 'ankle_angle_l',
    #'subtalar_angle_r', 'subtalar_angle_l'
]

# Movement mapping 
# Used to associate unnamed np array rows to joint movements/rows of interest
movement_mapping = {
    'hip_flexion_r': 7,
    'hip_adduction_r': 8,
    'hip_rotation_r': 9,
    'knee_angle_r': 10,
    'knee_adduction_r': 11,
    'ankle_angle_r': 13,
    'hip_flexion_l': 16,
    'hip_adduction_l': 17,
    'hip_rotation_l': 18,
    'knee_angle_l': 19,
    'knee_adduction_l': 20,
    'ankle_angle_l': 22,
}

# Initialize an empty dictionary to store SPM results
spm_results = {}

# Iterate over each row of interest
for row_name in rows_of_interest:
    # Extract the relevant row from transposed NumPy arrays using the movement_mapping
    vic_row = vic_spm_data[movement_mapping[row_name], :]
    oc_row = oc_spm_data[movement_mapping[row_name], :]

    print(f"Shape of {row_name} - VIC: {vic_row.shape}")
    print(f"Shape of {row_name} - OC: {oc_row.shape}")

    # Check normality with k2 test
    spmi_norm = spm1d.stats.normality.ttest_paired(vic_row, oc_row).inference(alpha=0.05)
    print(spmi_norm)

    if spmi_norm.h0reject:
        # Run the parametric dep ttest
        spm = spm1d.stats.ttest_paired(vic_row, oc_row)
        spmi = spm.inference(alpha=0.05, two_tailed=True)
        print(spmi)

    else:
        # Run the nonparametric dep ttest
        snpm = spm1d.stats.nonparam.ttest_paired(vic_row, oc_row)
        snpmi = snpm.inference(alpha=0.05, two_tailed=True, iterations=500)
        print(snpmi)

    # Store the SPM result in the dictionary
    spm_results[row_name] = spmi if spmi_norm.h0reject else snpmi

I first create the needed datastructure for SMP1D (horizontally laid out numpy array) and a list to associate the unnamed rows to the previous column headers from my pd dataframes (this increases readability for me as I am not too experienced in Python).

I then want to loop across my specified rows of interest. I first check for normality and then perform either a parametric or non-parametric paired ttest.

So far for my intentions with this code, if I need to elaborate on certain aspects more please let me know and I'll do my best to clarify.

When running this code this is the console output:

Shape of hip_flexion_r - VIC: (320,)
Shape of hip_flexion_r - OC: (320,)
SPM{X2} (0D) inference
   SPM.z        :  101.15444
   SPM.df       :  (1, 2)
Inference:
   SPM.alpha    :  0.050
   SPM.zstar    :  5.99146
   SPM.h0reject :  True
   SPM.p        :  0.00000

SPM{T} (0D) inference
   SPM.z        :  -7.18473
   SPM.df       :  (1, 319)
Inference:
   SPM.alpha    :  0.050
   SPM.zstar    :  1.96743
   SPM.h0reject :  True
   SPM.p        :  0.00000

Shape of hip_adduction_r - VIC: (320,)
Shape of hip_adduction_r - OC: (320,)
SPM{X2} (0D) inference
   SPM.z        :  12.88766
   SPM.df       :  (1, 2)
Inference:
   SPM.alpha    :  0.050
   SPM.zstar    :  5.99146
   SPM.h0reject :  True
   SPM.p        :  0.00159

SPM{T} (0D) inference
   SPM.z        :  -107.40437
   SPM.df       :  (1, 319)
Inference:
   SPM.alpha    :  0.050
   SPM.zstar    :  1.96743
   SPM.h0reject :  True
   SPM.p        :  0.00000

Shape of hip_rotation_r - VIC: (320,)
Shape of hip_rotation_r - OC: (320,)
SPM{X2} (0D) inference
   SPM.z        :  117.18431
   SPM.df       :  (1, 2)
Inference:
   SPM.alpha    :  0.050
   SPM.zstar    :  5.99146
   SPM.h0reject :  True
   SPM.p        :  0.00000

SPM{T} (0D) inference
   SPM.z        :  -21.26529
   SPM.df       :  (1, 319)
Inference:
   SPM.alpha    :  0.050
   SPM.zstar    :  1.96743
   SPM.h0reject :  True
   SPM.p        :  0.00000

Shape of hip_flexion_l - VIC: (320,)
Shape of hip_flexion_l - OC: (320,)
SPM{X2} (0D) inference
   SPM.z        :  32.78803
   SPM.df       :  (1, 2)
Inference:
   SPM.alpha    :  0.050
   SPM.zstar    :  5.99146
   SPM.h0reject :  True
   SPM.p        :  0.00000

SPM{T} (0D) inference
   SPM.z        :  -10.67827
   SPM.df       :  (1, 319)
Inference:
   SPM.alpha    :  0.050
   SPM.zstar    :  1.96743
   SPM.h0reject :  True
   SPM.p        :  0.00000

Shape of hip_adduction_l - VIC: (320,)
Shape of hip_adduction_l - OC: (320,)
SPM{X2} (0D) inference
   SPM.z        :  5.57507
   SPM.df       :  (1, 2)
Inference:
   SPM.alpha    :  0.050
   SPM.zstar    :  5.99146
   SPM.h0reject :  False
   SPM.p        :  0.06157

SnPM{t} inference (0D)
   SPM.z              :  -60.090
   SnPM.nPermUnique   :  2.136e+96 permutations possible
Inference:
   SnPM.nPermActual   :  500 actual permutations
   SPM.alpha          :  0.050
   SPM.zstar (lower)  :  -2.35024
   SPM.zstar (upper)  :  2.20365
   SPM.two_tailed     :  True
   SPM.h0reject       :  True
   SPM.p              :  0.002

Shape of hip_rotation_l - VIC: (320,)
Shape of hip_rotation_l - OC: (320,)
SPM{X2} (0D) inference
   SPM.z        :  62.09990
   SPM.df       :  (1, 2)
Inference:
   SPM.alpha    :  0.050
   SPM.zstar    :  5.99146
   SPM.h0reject :  True
   SPM.p        :  0.00000

SPM{T} (0D) inference
   SPM.z        :  -46.79220
   SPM.df       :  (1, 319)
Inference:
   SPM.alpha    :  0.050
   SPM.zstar    :  1.96743
   SPM.h0reject :  True
   SPM.p        :  0.00000

Shape of knee_angle_r - VIC: (320,)
Shape of knee_angle_r - OC: (320,)
SPM{X2} (0D) inference
   SPM.z        :  121.73188
   SPM.df       :  (1, 2)
Inference:
   SPM.alpha    :  0.050
   SPM.zstar    :  5.99146
   SPM.h0reject :  True
   SPM.p        :  0.00000

SPM{T} (0D) inference
   SPM.z        :  0.05023
   SPM.df       :  (1, 319)
Inference:
   SPM.alpha    :  0.050
   SPM.zstar    :  1.96743
   SPM.h0reject :  False
   SPM.p        :  0.95997

Shape of knee_adduction_r - VIC: (320,)
Shape of knee_adduction_r - OC: (320,)
SPM{X2} (0D) inference
   SPM.z        :  171.71350
   SPM.df       :  (1, 2)
Inference:
   SPM.alpha    :  0.050
   SPM.zstar    :  5.99146
   SPM.h0reject :  True
   SPM.p        :  0.00000

SPM{T} (0D) inference
   SPM.z        :  -9.70084
   SPM.df       :  (1, 319)
Inference:
   SPM.alpha    :  0.050
   SPM.zstar    :  1.96743
   SPM.h0reject :  True
   SPM.p        :  0.00000

Shape of knee_angle_l - VIC: (320,)
Shape of knee_angle_l - OC: (320,)
SPM{X2} (0D) inference
   SPM.z        :  23.90636
   SPM.df       :  (1, 2)
Inference:
   SPM.alpha    :  0.050
   SPM.zstar    :  5.99146
   SPM.h0reject :  True
   SPM.p        :  0.00001

SPM{T} (0D) inference
   SPM.z        :  -4.93954
   SPM.df       :  (1, 319)
Inference:
   SPM.alpha    :  0.050
   SPM.zstar    :  1.96743
   SPM.h0reject :  True
   SPM.p        :  0.00000

Shape of knee_adduction_l - VIC: (320,)
Shape of knee_adduction_l - OC: (320,)
SPM{X2} (0D) inference
   SPM.z        :  93.77705
   SPM.df       :  (1, 2)
Inference:
   SPM.alpha    :  0.050
   SPM.zstar    :  5.99146
   SPM.h0reject :  True
   SPM.p        :  0.00000

SPM{T} (0D) inference
   SPM.z        :  -12.57172
   SPM.df       :  (1, 319)
Inference:
   SPM.alpha    :  0.050
   SPM.zstar    :  1.96743
   SPM.h0reject :  True
   SPM.p        :  0.00000

Shape of ankle_angle_r - VIC: (320,)
Shape of ankle_angle_r - OC: (320,)
SPM{X2} (0D) inference
   SPM.z        :  2.93254
   SPM.df       :  (1, 2)
Inference:
   SPM.alpha    :  0.050
   SPM.zstar    :  5.99146
   SPM.h0reject :  False
   SPM.p        :  0.23078

SnPM{t} inference (0D)
   SPM.z              :  16.456
   SnPM.nPermUnique   :  2.136e+96 permutations possible
Inference:
   SnPM.nPermActual   :  500 actual permutations
   SPM.alpha          :  0.050
   SPM.zstar (lower)  :  -2.33346
   SPM.zstar (upper)  :  2.21557
   SPM.two_tailed     :  True
   SPM.h0reject       :  True
   SPM.p              :  0.002

Shape of ankle_angle_l - VIC: (320,)
Shape of ankle_angle_l - OC: (320,)
SPM{X2} (0D) inference
   SPM.z        :  221.94342
   SPM.df       :  (1, 2)
Inference:
   SPM.alpha    :  0.050
   SPM.zstar    :  5.99146
   SPM.h0reject :  True
   SPM.p        :  0.00000

SPM{T} (0D) inference
   SPM.z        :  17.30938
   SPM.df       :  (1, 319)
Inference:
   SPM.alpha    :  0.050
   SPM.zstar    :  1.96743
   SPM.h0reject :  True
   SPM.p        :  0.00000

When inspecting the spm_results dictionary, the accessible data looks much different than that from the examples provided (please see attached screenshot). Example spm result

My main concern is that the .plot entry is missing which is why I cannot visualize my results.

Is there an issue in my code which leads to this and if so: What can I do to correct it? Are there other concerns to my approach?

As you can probably tell, I am fairly new to posting such questions on GitHub. I hope my issue is not beyond the scope of this forum.

Thank you in advance for any support and have a happy new year.

0todd0000 commented 6 months ago

If the plot method is missing it means that spm1d has interpreted the data as 0D and not as a set of 1D time series observations. You'll see this in the SPM object print-outs, for example:

SPM{T} (0D) inference
   SPM.z        :  -12.57172
   SPM.df       :  (1, 319)
Inference:
   SPM.alpha    :  0.050
   SPM.zstar    :  1.96743
   SPM.h0reject :  True
   SPM.p        :  0.00000

Notice the "0D" in the header.

These data are 0D because they are 1D arrays (i.e., several observations of 0D values). In order to run 1D analysis you need 2D arrays (i.e., several observations of 1D values). The required array shapes are:

where:

BeFaer commented 6 months ago

Hi Todd,

thank you for your reply. If I understand correctly, this would mean not to use the overall grand mean data (calculated across all trials and participants for both systems individually over time), but rather to collect the individual trial data on the given joint DOF from both systems and feed this data into the SPM operation.

Is that the correct approach in order to get a 1D SPM object which has the plot method?

Thank you for your support.

Best

Bernhard

0todd0000 commented 6 months ago

I am unsure whether using or not using the grand mean is relevant because it is a bit unclear what it is and how it is used. If the grand mean is a scalar or a 1D trajectory then its use should not matter. Regardless, if you submit 2D arrays to spm1d's univariate procedures (like t tests) then the data will be interpreted as 1D and 1D analysis will be conducted. I suggest simplifying the code you attached to conduct just a single test (on hip_flexion_r, for example). If you can create 2D arrays for this test and thereby conduct 1D analysis then it should become straightforward how to analyze other cases.