neurowarp 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,9 @@
1
+ .DS_Store
2
+ .gitattributes
3
+ .gitignore
4
+ *__pycache__
5
+ *README~.md
6
+ *dist
7
+ *instructions_for_myself.docx
8
+ ~$structions_for_myself.docx
9
+ nans_data.mat
@@ -0,0 +1,107 @@
1
+ Metadata-Version: 2.3
2
+ Name: neurowarp
3
+ Version: 0.0.1
4
+ Summary: Toolbox for Dynamic Time Warping based latency differences and temporal correlations between two time series for neuroscience
5
+ Project-URL: Homepage, https://github.com/mahan-hosseini/NeuroWarp/
6
+ Project-URL: Issues, https://github.com/mahan-hosseini/NeuroWarp/Issues
7
+ Author-email: Mahan Hosseini <m.hosseini@fz-juelich.de>
8
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3
11
+ Requires-Python: >=3.10
12
+ Requires-Dist: matplotlib>=3.7
13
+ Requires-Dist: numpy>=1.24
14
+ Requires-Dist: scipy>=1.11
15
+ Requires-Dist: tslearn>=0.6.3
16
+ Description-Content-Type: text/markdown
17
+
18
+ # Python NeuroWarp
19
+
20
+ Python NeuroWarp is provided as a package that can be installed via PyPi. It consists of two functions that can be called after importing neurowarp: timeseries_correlation and latency_difference - these are the general-purpose scripts enable the DTW analyses presented in *Transient Attention Gates Access Consciousness: Coupling N2pc and P3 Latencies using Dynamic Time Warping*.
21
+
22
+ ## Installation
23
+
24
+ We recommend that you create a new virtual environment for our module via:
25
+ 1. Open a terminal / cmd.exe window, navigate (via `cd`) to the directory you want to create the environment and enter:
26
+ `python -m venv neurowarp_env`
27
+ 2. Activate the neurowarp_env via:
28
+
29
+ **Windows:** `path\to\neurowarp_env\Scripts\activate`
30
+
31
+ **MacOS:** `source neurowarp_env/bin/activate`
32
+
33
+ 3. Install neurowarp via pip (enter while neurowarp_env is active):
34
+ `pip install neurowarp`
35
+ 4. The functions can be used as explained below after importing the module
36
+
37
+ For more detail on virtual environments & pip [click here](https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/)
38
+
39
+
40
+ ## DTW Temporal Correlation - neurowarp.timeseries_correlation()
41
+ Perform a **DTW based bootstrap analysis** to assess the **temporal correlation between two time series** (Figure 5 of our paper).
42
+
43
+ ### Important Notes
44
+ - Time series must be **2D matrices**
45
+ - I.e., data points (e.g. time) x subjects (i.e., replications)
46
+ - We provide the ERPs of correct and intrusion trials for users to explore this function
47
+
48
+ ### Running timeseries_correlation using our ERPs
49
+ *Enter the following into Python and make sure to enter your actual paths!*
50
+ 1. `import neurowarp`
51
+ 2. `from scipy.io import loadmat`
52
+ 3. `data = loadmat("your/path/to/example_series_N2pcP3s")`
53
+ 4. `series_1 = data["P3_Correct"]`
54
+ 5. `series_2 = data["N2pc_Correct"]`
55
+ 6. `name_1 = "P3"`
56
+ 7. `name_2 = "N2pc"`
57
+ 8. `savepath = "where/to/store/results/to"`
58
+ 9. `num_boots = 10000`
59
+ - The number of bootstrap samples that you want to implement
60
+ 10. `outlier = 0`
61
+ - Exclude outliers if their DTW area is +-5 standard deviations from the mean
62
+ 11. `try_to_fix_ylims = 1`
63
+ - Attempt to standardise y-limits of marginals
64
+ 12. `neurowarp.timeseries_correlation(series_1, series_2, name_1, name_2, savepath, num_boots, outlier, try_to_fix_ylims)`
65
+
66
+ *Note that the figure will look slightly different to that of our paper due to different x/y limits. See the replicate_figures folder if you want to replicate our figure as it was printed.*
67
+
68
+ ## DTW Latency Difference - neurowarp.latency_difference()
69
+ Assess the **latency difference** between **two conditions** (i.e., within-subjects effect) or between **two groups** (i.e., across-subjects effect) of any signal of interest (in milliseconds).
70
+
71
+ *Figures 3 & 4 of our paper show a two conditions analysis*
72
+
73
+ ### Important Notes
74
+ - Reference and query time series must be **2D matrices**
75
+ - I.e., data points (e.g., time) x subjects (i.e., replications)
76
+ - Time series have to be of **equal sizes**
77
+ - **analysis_design** determines whether you want to assess a within- or between-subjects latency effect (can only take “within” or “between” as input)
78
+ - We provide the ERPs of correct and intrusion trials for users to explore this function
79
+
80
+ ### Running timeseries_correlation using our ERPs
81
+ *Enter the following into Python and make sure to enter your actual paths!*
82
+ 1. `import neurowarp`
83
+ 2. `from scipy.io import loadmat`
84
+ 3. `data = loadmat("your/path/to/example_series_N2pcP3s")`
85
+ 4. `analysis_design = "within"`
86
+ 5. `query = data["N2pc_Intrusion"]`
87
+ 6. `reference = data["N2pc_Correct"]`
88
+ 7. `name_query = "Intrusion"`
89
+ 8. `name_reference = "Correct"`
90
+ 9. `units = "\u03BCV"`
91
+ - The units that your signals are measured in (in our case micro volts)
92
+ 10. `sampling_rate = 500`
93
+ - The number of data points per second in Hertz
94
+ 11. `savepath = "where/to/store/results/to"`
95
+ 12. `permutations = 10000`
96
+ - The number of permutations you would like to implement in statistical testing (we recommend >=10000)
97
+ 13. `neurowarp.latency_difference(analysis_design, query, reference, name_query, name_reference,units, sampling_rate, savepath, permutations)`
98
+
99
+ ## Dependencies
100
+ *Python NeuroWarp requires the following toolboxes which are automatically installed via `pip install neurowarp`*
101
+ - Numpy
102
+ - Matplotlib
103
+ - Tslearn
104
+ - Scipy
105
+
106
+ ## Tests
107
+ Python NeuroWarp was tested with Python 3.10.9.
@@ -0,0 +1,90 @@
1
+ # Python NeuroWarp
2
+
3
+ Python NeuroWarp is provided as a package that can be installed via PyPi. It consists of two functions that can be called after importing neurowarp: timeseries_correlation and latency_difference - these are the general-purpose scripts enable the DTW analyses presented in *Transient Attention Gates Access Consciousness: Coupling N2pc and P3 Latencies using Dynamic Time Warping*.
4
+
5
+ ## Installation
6
+
7
+ We recommend that you create a new virtual environment for our module via:
8
+ 1. Open a terminal / cmd.exe window, navigate (via `cd`) to the directory you want to create the environment and enter:
9
+ `python -m venv neurowarp_env`
10
+ 2. Activate the neurowarp_env via:
11
+
12
+ **Windows:** `path\to\neurowarp_env\Scripts\activate`
13
+
14
+ **MacOS:** `source neurowarp_env/bin/activate`
15
+
16
+ 3. Install neurowarp via pip (enter while neurowarp_env is active):
17
+ `pip install neurowarp`
18
+ 4. The functions can be used as explained below after importing the module
19
+
20
+ For more detail on virtual environments & pip [click here](https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/)
21
+
22
+
23
+ ## DTW Temporal Correlation - neurowarp.timeseries_correlation()
24
+ Perform a **DTW based bootstrap analysis** to assess the **temporal correlation between two time series** (Figure 5 of our paper).
25
+
26
+ ### Important Notes
27
+ - Time series must be **2D matrices**
28
+ - I.e., data points (e.g. time) x subjects (i.e., replications)
29
+ - We provide the ERPs of correct and intrusion trials for users to explore this function
30
+
31
+ ### Running timeseries_correlation using our ERPs
32
+ *Enter the following into Python and make sure to enter your actual paths!*
33
+ 1. `import neurowarp`
34
+ 2. `from scipy.io import loadmat`
35
+ 3. `data = loadmat("your/path/to/example_series_N2pcP3s")`
36
+ 4. `series_1 = data["P3_Correct"]`
37
+ 5. `series_2 = data["N2pc_Correct"]`
38
+ 6. `name_1 = "P3"`
39
+ 7. `name_2 = "N2pc"`
40
+ 8. `savepath = "where/to/store/results/to"`
41
+ 9. `num_boots = 10000`
42
+ - The number of bootstrap samples that you want to implement
43
+ 10. `outlier = 0`
44
+ - Exclude outliers if their DTW area is +-5 standard deviations from the mean
45
+ 11. `try_to_fix_ylims = 1`
46
+ - Attempt to standardise y-limits of marginals
47
+ 12. `neurowarp.timeseries_correlation(series_1, series_2, name_1, name_2, savepath, num_boots, outlier, try_to_fix_ylims)`
48
+
49
+ *Note that the figure will look slightly different to that of our paper due to different x/y limits. See the replicate_figures folder if you want to replicate our figure as it was printed.*
50
+
51
+ ## DTW Latency Difference - neurowarp.latency_difference()
52
+ Assess the **latency difference** between **two conditions** (i.e., within-subjects effect) or between **two groups** (i.e., across-subjects effect) of any signal of interest (in milliseconds).
53
+
54
+ *Figures 3 & 4 of our paper show a two conditions analysis*
55
+
56
+ ### Important Notes
57
+ - Reference and query time series must be **2D matrices**
58
+ - I.e., data points (e.g., time) x subjects (i.e., replications)
59
+ - Time series have to be of **equal sizes**
60
+ - **analysis_design** determines whether you want to assess a within- or between-subjects latency effect (can only take “within” or “between” as input)
61
+ - We provide the ERPs of correct and intrusion trials for users to explore this function
62
+
63
+ ### Running timeseries_correlation using our ERPs
64
+ *Enter the following into Python and make sure to enter your actual paths!*
65
+ 1. `import neurowarp`
66
+ 2. `from scipy.io import loadmat`
67
+ 3. `data = loadmat("your/path/to/example_series_N2pcP3s")`
68
+ 4. `analysis_design = "within"`
69
+ 5. `query = data["N2pc_Intrusion"]`
70
+ 6. `reference = data["N2pc_Correct"]`
71
+ 7. `name_query = "Intrusion"`
72
+ 8. `name_reference = "Correct"`
73
+ 9. `units = "\u03BCV"`
74
+ - The units that your signals are measured in (in our case micro volts)
75
+ 10. `sampling_rate = 500`
76
+ - The number of data points per second in Hertz
77
+ 11. `savepath = "where/to/store/results/to"`
78
+ 12. `permutations = 10000`
79
+ - The number of permutations you would like to implement in statistical testing (we recommend >=10000)
80
+ 13. `neurowarp.latency_difference(analysis_design, query, reference, name_query, name_reference,units, sampling_rate, savepath, permutations)`
81
+
82
+ ## Dependencies
83
+ *Python NeuroWarp requires the following toolboxes which are automatically installed via `pip install neurowarp`*
84
+ - Numpy
85
+ - Matplotlib
86
+ - Tslearn
87
+ - Scipy
88
+
89
+ ## Tests
90
+ Python NeuroWarp was tested with Python 3.10.9.
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "neurowarp"
7
+ version = "0.0.1"
8
+ authors = [
9
+ { name="Mahan Hosseini", email="m.hosseini@fz-juelich.de" },
10
+ ]
11
+ description = "Toolbox for Dynamic Time Warping based latency differences and temporal correlations between two time series for neuroscience"
12
+ dependencies = [
13
+ "numpy>=1.24",
14
+ "matplotlib>=3.7",
15
+ "scipy>=1.11",
16
+ "tslearn>=0.6.3",
17
+ ]
18
+ readme = "README.md"
19
+ requires-python = ">=3.10"
20
+ classifiers = [
21
+ "Programming Language :: Python :: 3",
22
+ "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
23
+ "Operating System :: OS Independent",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/mahan-hosseini/NeuroWarp/"
28
+ Issues = "https://github.com/mahan-hosseini/NeuroWarp/Issues"
@@ -0,0 +1,2 @@
1
+ from .latency_difference import latency_difference
2
+ from .timeseries_correlation import timeseries_correlation
@@ -0,0 +1,481 @@
1
+ # %% ************ LATENCY DIFFERENCES WITH DYNAMIC TIME WARPING (IN MS) **************
2
+
3
+ # %% Imports
4
+ import pdb
5
+ import os
6
+ import pickle
7
+ import numpy as np
8
+ import matplotlib.pyplot as plt
9
+ from tslearn.metrics import dtw_path
10
+ import scipy.stats as stats
11
+
12
+ # %% Global constants
13
+ TITLE_FONTSIZE = 20
14
+ SUPLABEL_FONTSIZE = 15
15
+
16
+
17
+ # %% Main function
18
+ def latency_difference(
19
+ analysis_design,
20
+ query,
21
+ reference,
22
+ name_query,
23
+ name_reference,
24
+ units,
25
+ sampling_rate,
26
+ savepath,
27
+ permutations,
28
+ ):
29
+ """
30
+ Conceptual Approach
31
+ -------------------
32
+ To assess a latency difference between 2 time series:
33
+ 1) Average across subjects & compute DTW between the averages
34
+ 2) The median of the distribution of the warping path's x/y distances
35
+ is used to compute the latency difference in milliseconds
36
+ 3) The area between the warping path and the main diagonal is used
37
+ in a permutation procedure for assigning a p value to the latency
38
+ difference
39
+
40
+ Note
41
+ ----
42
+ 1) This function allows assessment of latency differences in within-
43
+ or between-subjects designs. Set the "analysis_design" input parameter
44
+ accordingly. This affects permutation procedure, adopting the
45
+ permutation equivalents of paired- or independent-sample t-tests
46
+ respectively.
47
+ 2) This whole function is in line with the proposition of
48
+ Zoumpoulaki et al. (2015)
49
+ Latency as a region contrast: Measuring ERP latency differences with
50
+ Dynamic Time Warping
51
+ 3) Your full time series are analysed.
52
+ If you would like to assess only a specific interval of your time
53
+ series, use indexing before calling this function.
54
+
55
+ Input Parameters
56
+ ----------------
57
+ 1) analysis_design
58
+ => Character array or string informing about your analysis design
59
+ => Has to be either "within" or "between", based on whether you have
60
+ a within- or between-subjects design
61
+ 2) query
62
+ => Query time series (matrix w. dimensions: datapoints x subjects)
63
+ 3) reference
64
+ => Reference time series (matrix w. dimensions: datapoints x subjects)
65
+ 4) name_query
66
+ => Name of the query time series
67
+ 5) name_reference
68
+ => Name of the reference time series
69
+ 6) units
70
+ => In what units was your data measured?
71
+ - For microvolts enter '\muV'
72
+ 7) sampling_rate
73
+ => What was the sampling rate of your signal? In Hertz
74
+ => Used to compute the latency difference in milliseconds
75
+ 8) savepath
76
+ => Path with files
77
+ 9) permutations
78
+ => Number of permutations to adopt in statistical testing
79
+ - We recommend at least 10000
80
+ """
81
+
82
+ # %% Preparation
83
+
84
+ analysis_design = str(analysis_design)
85
+ units = str(units)
86
+ name_query = str(name_query)
87
+ name_reference = str(name_reference)
88
+ savepath = str(savepath)
89
+ plotpath = os.path.join(savepath, "Plots/")
90
+ varpath = os.path.join(savepath, "Variables/")
91
+ if not os.path.exists(plotpath):
92
+ os.makedirs(plotpath)
93
+ if not os.path.exists(varpath):
94
+ os.makedirs(varpath)
95
+
96
+ # Sanity check on analysis_design value
97
+ if analysis_design not in ["between", "within"]:
98
+ print('analysis_design must be either "between" or "within"! Fix & re-run')
99
+ return
100
+
101
+ # Sanity check if series are identical for within and have same length
102
+ try:
103
+ query = np.array(query)
104
+ reference = np.array(reference)
105
+ except:
106
+ raise ValueError(
107
+ "Query & Ref input vars must be convertible to np arrays - fix & rerun!"
108
+ )
109
+ # => can have different number of subjects if between
110
+ if analysis_design == "within":
111
+ if query.shape != reference.shape:
112
+ print(
113
+ "Note - query & reference don't have identical shapes - fix & re-run!"
114
+ )
115
+ return
116
+ else: # has to be 'between' now (
117
+ if query.shape[0] != reference.shape[0]:
118
+ print(
119
+ "Note - query & reference do not have same datapoint-length - fix & "
120
+ + "re-run!"
121
+ )
122
+ return
123
+
124
+ # %% MAIN DTW COMPUTATION
125
+ # We need indices of x & y coordinates of warping path!
126
+ # ORIGINAL FORM: WP, score = dtw(x,y) (we don't need score)
127
+ # ==> QUERY IS ON X-AXIS OF WP, SO FIRST INPUT
128
+ # ==> REFERENCE IS ON Y-AXIS OF WP, SO SECOND INPUT!
129
+ # ==> WP is a list of lists with [ix, iy] coordinates of WP
130
+ # ==> WP of this will be below main diagonal if QUERY is
131
+ # LATER than REFERENCE
132
+ # ==> I.e. AREA will be positive in this case because area = areaDIAG -
133
+ # areaWP (and hence areaDIAG will be larger than areaWP if WP is below
134
+ # DIAG [and QUERY is indeed LATER than REFERENCE!]
135
+
136
+ # average across subjects
137
+ plot_avg_query = query.mean(axis=1)
138
+ plot_avg_reference = reference.mean(axis=1)
139
+
140
+ # DTW has to be done on standardised averages
141
+ z_avg_query = stats.zscore(plot_avg_query)
142
+ z_avg_reference = stats.zscore(plot_avg_reference)
143
+ WP, score = dtw_path(z_avg_query, z_avg_reference)
144
+
145
+ # extract indices of WP's x & y coordinates
146
+ i_query_x = []
147
+ i_reference_y = []
148
+ for WP_idxs in WP:
149
+ i_query_x.append(WP_idxs[0])
150
+ i_reference_y.append(WP_idxs[1])
151
+ warped_query = z_avg_query[i_query_x] # apply indices to get aligned time series
152
+ warped_reference = z_avg_reference[i_reference_y]
153
+
154
+ # plot standardised & aligned time series
155
+ plot_standardised_and_aligned_series(
156
+ z_avg_query, z_avg_reference, warped_query, warped_reference, units, plotpath
157
+ )
158
+
159
+ # Latency in MS is distribution of point-wise distances:
160
+ # (distance = ix - iy) / sampling rate (Hz) / 1000
161
+ distance = np.array(i_query_x) - np.array(i_reference_y)
162
+ latency = np.median(distance)
163
+ lat_in_ms = latency / (sampling_rate / 1000)
164
+
165
+ print("\n *** Latency Difference: %i ms! *** \n" % (lat_in_ms))
166
+
167
+ # Plot WP compared to main diagonal
168
+ plotWP(
169
+ i_query_x,
170
+ i_reference_y,
171
+ plot_avg_query,
172
+ plot_avg_reference,
173
+ name_query,
174
+ name_reference,
175
+ units,
176
+ lat_in_ms,
177
+ plotpath,
178
+ )
179
+
180
+ # %% PERMUTATION TEST ON AREA MEASURE
181
+ # ==> have this for median(distance), i.e., lat_in_ms_measure, too
182
+
183
+ # compute true observed area
184
+ xmax = i_query_x[-1] + 1 # because this is used with range
185
+ DIAG = np.arange(xmax)
186
+ areaDIAG = np.trapz(DIAG)
187
+ # IMPORTANT NOTE ABOUT NP.TRAPZ - TAKES IN (Y) & (Y,X) WHEREAS MATLAB'S TRAPZ
188
+ # TAKES (Y) & (X,Y) !!!!!
189
+ areaWP = np.trapz(i_reference_y, i_query_x)
190
+ area = (areaDIAG - areaWP) / areaDIAG
191
+
192
+ # prepare vars for permuting
193
+ original_query = query.copy() # copy so it's not just a reference
194
+ original_reference = reference.copy()
195
+ perm_x = [[] for _ in range(permutations)]
196
+ perm_y = [[] for _ in range(permutations)]
197
+ perm_area = [[] for _ in range(permutations)]
198
+ datapoints = np.shape(query)[0]
199
+ subjects = np.shape(query)[1]
200
+
201
+ # permutation loop
202
+ for perms in range(permutations):
203
+ # for a within-subject contrast, do the coin flipping for permuting class labels
204
+ if analysis_design == "within":
205
+ perm_query = np.zeros((datapoints, subjects))
206
+ perm_reference = np.zeros((datapoints, subjects))
207
+ for s in range(subjects):
208
+ coin = np.random.randint(1, 3, subjects) # 3 bc. indexing exclusive
209
+ if coin[s] == 1:
210
+ perm_query[:, s] = original_query[:, s]
211
+ perm_reference[:, s] = original_reference[:, s]
212
+ elif coin[s] == 2:
213
+ perm_query[:, s] = original_reference[:, s]
214
+ perm_reference[:, s] = original_query[:, s]
215
+ # for a between-subject contrast, randomly assign subjects to groups, preserving
216
+ # the original group sizes of query & reference
217
+ else:
218
+ subjects_query = np.shape(query)[1]
219
+ subjects_reference = np.shape(reference)[1]
220
+ subjects_total = subjects_query + subjects_reference
221
+ this_perm = np.random.permutation(subjects_total)
222
+ data_total = np.concatenate((query, reference), axis=1)
223
+ perm_query = data_total[:, this_perm[:subjects_query]]
224
+ perm_reference = data_total[:, this_perm[subjects_query:]]
225
+
226
+ # get averages across subjects
227
+ perm_query_avg = perm_query.mean(axis=1)
228
+ perm_reference_avg = perm_reference.mean(axis=1)
229
+
230
+ # DTW has to be done on standardised averages
231
+ perm_query_z_avg = stats.zscore(perm_query_avg)
232
+ perm_reference_z_avg = stats.zscore(perm_reference_avg)
233
+ perm_WP, perm_score = dtw_path(perm_query_z_avg, perm_reference_z_avg)
234
+
235
+ # extract indices of permuted WP's x & y coordinates - store in lists
236
+ perm_i_query_x = []
237
+ perm_i_reference_y = []
238
+ for WP_idxs in perm_WP:
239
+ perm_i_query_x.append(WP_idxs[0])
240
+ perm_i_reference_y.append(WP_idxs[1])
241
+ perm_x[perms] = perm_i_query_x
242
+ perm_y[perms] = perm_i_reference_y
243
+
244
+ # compute permuted area - store results in list
245
+ perm_xmax = perm_i_query_x[-1] + 1 # because this is used with range
246
+ perm_DIAG = np.arange(perm_xmax)
247
+ perm_areaDIAG = np.trapz(perm_DIAG)
248
+ # IMPORTANT NOTE ABOUT NP.TRAPZ - TAKES IN (Y) & (Y,X) WHEREAS MATLAB'S TRAPZ
249
+ # TAKES (Y) & (X,Y) !!!!!
250
+ perm_areaWP = np.trapz(perm_i_reference_y, perm_i_query_x)
251
+ perm_area[perms] = (perm_areaDIAG - perm_areaWP) / perm_areaDIAG
252
+
253
+ # permutation loop end
254
+ permdist_areas = np.sort(perm_area) # sort nonabsolutes for idxing thresholds
255
+ sort_abs_permdist_areas = np.sort(abs(permdist_areas))
256
+ thresh_abs = round(len(permdist_areas) * 0.95)
257
+ # find 5% sig. thresh using sorted absolutes
258
+ thresh_abs_val = sort_abs_permdist_areas[thresh_abs]
259
+ thresh1 = -thresh_abs_val
260
+ thresh2 = thresh_abs_val
261
+
262
+ # p value (two tailed test)
263
+ p = sum(abs(permdist_areas) >= abs(area)) / permutations
264
+ if p < 0.0001:
265
+ plot_p = "p < 0.0001"
266
+ else:
267
+ plot_p = "p = " + str(round(p, 3))
268
+ print("\n *** %s *** \n" % (plot_p))
269
+
270
+ # Plot figure of observed area-size & its permutation distribution
271
+ f, ax = plt.subplots()
272
+ plt.tight_layout()
273
+ ax.hist(permdist_areas, bins=30, facecolor="#c7fdb5", edgecolor="#e4cbff")
274
+ ax.axvline(thresh1, linewidth=1.5, linestyle="-", color="#ff796c")
275
+ ax.axvline(thresh2, linewidth=1.5, linestyle="-", color="#ff796c")
276
+ ax.axvline(area, linewidth=1.5, linestyle="-", color="#8ab8fe")
277
+ yticks = ax.get_yticks()
278
+ yticklabels_num = yticks / permutations
279
+ yticklabels_str = [str(tick) for tick in yticklabels_num]
280
+ ax.set_yticks(yticks, labels=yticklabels_str)
281
+ ax.set_ylabel("Proportion", fontsize=12)
282
+ ax.set_xlabel("DTW - Area", fontsize=12)
283
+ ax.set_title(
284
+ "Latency diff.: %i ms - %s" % (lat_in_ms, plot_p),
285
+ fontweight="bold",
286
+ fontsize=13,
287
+ )
288
+ f.savefig(
289
+ os.path.join(plotpath, "Permutation Distribution.png"),
290
+ bbox_inches="tight",
291
+ dpi=300,
292
+ )
293
+ f.savefig(
294
+ os.path.join(plotpath, "Permutation Distribution.svg"),
295
+ bbox_inches="tight",
296
+ dpi=300,
297
+ )
298
+ plt.show(block=False)
299
+ plt.pause(0.001)
300
+
301
+ # Plot figure of observed area-size & its permutation distribution
302
+ f, ax = plt.subplots()
303
+ plt.tight_layout()
304
+ ax.hist(abs(permdist_areas), bins=30, facecolor="#c7fdb5", edgecolor="#e4cbff")
305
+ ax.axvline(thresh_abs_val, linewidth=1.5, linestyle="-", color="#ff796c")
306
+ ax.axvline(abs(area), linewidth=1.5, linestyle="-", color="#8ab8fe")
307
+ yticks = ax.get_yticks()
308
+ yticklabels_num = yticks / permutations
309
+ yticklabels_str = [str(tick) for tick in yticklabels_num]
310
+ ax.set_yticks(yticks, labels=yticklabels_str)
311
+ ax.set_ylabel("Proportion", fontsize=12)
312
+ ax.set_xlabel("DTW - Area (absolute)", fontsize=12)
313
+ ax.set_title(
314
+ "Absolute Areas - Latency diff.: %i ms - %s" % (lat_in_ms, plot_p),
315
+ fontweight="bold",
316
+ fontsize=13,
317
+ )
318
+ f.savefig(
319
+ os.path.join(plotpath, "Permutation Distribution (absolutes).png"),
320
+ bbox_inches="tight",
321
+ dpi=300,
322
+ )
323
+ f.savefig(
324
+ os.path.join(plotpath, "Permutation Distribution (absolutes).svg"),
325
+ bbox_inches="tight",
326
+ dpi=300,
327
+ )
328
+ plt.show(block=False)
329
+ plt.pause(0.001)
330
+
331
+ # store variables to a pickle file
332
+ vars_to_save = {
333
+ "area": area,
334
+ "p": p,
335
+ "thresh1": thresh1,
336
+ "thresh2": thresh2,
337
+ "perm_x": perm_x,
338
+ "perm_y": perm_y,
339
+ "permdist_areas": permdist_areas,
340
+ }
341
+ picklepath = os.path.join(
342
+ varpath, ("DTW Permutation Test - %s subjects.pkl" % analysis_design)
343
+ )
344
+ with open(picklepath, "wb") as file:
345
+ pickle.dump(vars_to_save, file)
346
+ print(
347
+ "\n\n****************************"
348
+ + "\nDTW Latency Analysis is done"
349
+ + "\n****************************\n\nResults were saved to:\n"
350
+ + savepath
351
+ )
352
+
353
+
354
+ # %% Local functions - plotting
355
+ def plot_standardised_and_aligned_series(
356
+ z_avg_query, z_avg_reference, warped_query, warped_reference, units, plotpath
357
+ ):
358
+ """Plots a figure of original/standardised and aligned time series"""
359
+ f, ax = plt.subplots(4, 1)
360
+ plt.tight_layout()
361
+ plt.subplots_adjust(left=0.1, bottom=0.1, top=0.9)
362
+ ax[0].plot(z_avg_query, color="k") # plot the data
363
+ ax[1].plot(z_avg_reference, color="r")
364
+ ax[2].plot(warped_query, color="k")
365
+ ax[3].plot(warped_reference, color="r")
366
+ # ylims of standardised time series
367
+ stand_min = min([z_avg_query.min(), z_avg_reference.min(), -1])
368
+ stand_max = max([z_avg_query.max(), z_avg_reference.max(), 1])
369
+ ax[0].set_ylim([stand_min, stand_max])
370
+ ax[1].set_ylim([stand_min, stand_max])
371
+ # ylims of warped time series
372
+ warped_min = min([warped_query.min(), warped_reference.min(), -1])
373
+ warped_max = max([warped_query.max(), warped_reference.max(), -1])
374
+ ax[2].set_ylim([warped_min, warped_max])
375
+ ax[3].set_ylim([warped_min, warped_max])
376
+ # labels, titles & save
377
+ ax[-1].set_xlabel("Datapoints", fontsize=SUPLABEL_FONTSIZE)
378
+ f.suptitle("Alignment", y=0.99, fontsize=TITLE_FONTSIZE)
379
+ f.supylabel(units + " (z-scored)", x=0.01, fontsize=SUPLABEL_FONTSIZE)
380
+ f.savefig(os.path.join(plotpath, "Alignment.png"), bbox_inches="tight", dpi=300)
381
+ f.savefig(os.path.join(plotpath, "Alignment.svg"), bbox_inches="tight", dpi=300)
382
+ plt.show(block=False) # otherwise stopped further code if python via terminal
383
+ plt.pause(0.001)
384
+
385
+
386
+ def plotWP(
387
+ i_query_x,
388
+ i_reference_y,
389
+ plot_avg_query,
390
+ plot_avg_reference,
391
+ name_query,
392
+ name_reference,
393
+ units,
394
+ lat_in_ms,
395
+ plotpath,
396
+ ):
397
+ """Plot the warping path, the main diagonal and both original time series"""
398
+ f, ax_global = plt.subplots()
399
+ ax_global.set_xticks([])
400
+ ax_global.set_yticks([])
401
+ gs = f.add_gridspec(4, 4) # prepare the grid
402
+ plt.tight_layout()
403
+ plt.subplots_adjust(left=0.1, bottom=0.1, top=0.9, hspace=0.5)
404
+
405
+ # *** grid space 1: WP & main diagonal ***
406
+ # main diagonal
407
+ ax = f.add_subplot(gs[0:3, 1:4]) # set wanted gridcells as ax
408
+ ax.set_xlim(right=i_query_x[-1])
409
+ ax.set_ylim(top=i_reference_y[-1])
410
+ xmax = i_query_x[-1] + 1 # bc. indexing starts at 0 & we use this var only w. range
411
+ ax.plot(
412
+ np.arange(xmax), np.arange(xmax), color="#ff796c", linewidth=1.5
413
+ ) # c==salmon
414
+ # warping path
415
+ ax.plot(i_query_x, i_reference_y, color="#8ab8fe", linewidth=1.5) # c==carolinablue
416
+ # fill area between WP & main diagonal
417
+ ax.fill_between(
418
+ i_query_x, i_query_x, i_reference_y, color="#c7fdb5", alpha=0.5, edgecolor="k"
419
+ )
420
+ # remove ticklabels
421
+ ax.set_xticklabels([])
422
+ ax.set_yticklabels([])
423
+
424
+ # *** grid space 2: query on x axis ***
425
+ ax_2 = f.add_subplot(gs[-1, 1:])
426
+ ax_2.plot(plot_avg_query, color="r")
427
+
428
+ # *** grid space 3: reference on y axis ***
429
+ ax_3 = f.add_subplot(gs[0:-1, 0])
430
+ # IMPORTANT NOTE - plt does not have camroll as matlab does. Thus we:
431
+ # 1. plot reference's y values (e.g. in mV) on "x axis"
432
+ # 2. plot len of idxs (xmax) as "y values"
433
+ # 3. we now HAVE TO invert the xaxis to get the same effect as with camroll
434
+ ax_3.plot(plot_avg_reference, np.arange(xmax), color="k")
435
+ ax_3.invert_xaxis()
436
+
437
+ # axis stuff
438
+ ylimmax = max([plot_avg_query.max(), plot_avg_reference.max()])
439
+ ylimmax = ylimmax + (ylimmax * 0.1)
440
+ ax_2.set_ylim(top=ylimmax)
441
+ ax_3.set_ylim(top=ylimmax)
442
+ xticks = ax.get_xticks() # set xaxis of ax_2 and yaxis of ax_3 according to WP axis
443
+ xlims = ax.get_xlim()
444
+ ax_2.set_xticks(xticks)
445
+ ax_2.set_xlim(xlims)
446
+ ax_3.set_yticks(xticks)
447
+ ax_3.set_ylim(xlims)
448
+ # query & reference text & labels
449
+ ax_2.text(
450
+ 0.77,
451
+ 1.1,
452
+ name_query,
453
+ transform=ax_2.transAxes,
454
+ fontsize=12,
455
+ fontweight="bold",
456
+ fontstyle="italic",
457
+ )
458
+ ax_3.text(
459
+ 0.2,
460
+ 1.03,
461
+ name_reference,
462
+ transform=ax_3.transAxes,
463
+ fontsize=12,
464
+ fontweight="bold",
465
+ fontstyle="italic",
466
+ )
467
+ ax_2.set_ylabel(units, labelpad=1)
468
+ ax_3.set_xlabel(units, labelpad=1)
469
+ for spine in ax_global.spines.values(): # despine outmost axis
470
+ spine.set_visible(False)
471
+
472
+ # suplabels & save the figure
473
+ ax.set_title(
474
+ "Latency difference: %i ms" % lat_in_ms, fontweight="bold", fontsize=13
475
+ )
476
+ ax_2.set_xlabel("Datapoints", fontsize=11)
477
+ ax_3.set_ylabel("Datapoints", fontsize=11)
478
+ f.savefig(os.path.join(plotpath, "WarpPath.png"), bbox_inches="tight", dpi=300)
479
+ f.savefig(os.path.join(plotpath, "WarpPath.svg"), bbox_inches="tight", dpi=300)
480
+ plt.show(block=False)
481
+ plt.pause(0.001)
@@ -0,0 +1,424 @@
1
+ # %% ************* CORRELATE TWO TIME SERIES WITH DYNAMIC TIME WARPING ***************
2
+
3
+ # %% Imports
4
+ import pdb
5
+ import os
6
+ import pickle
7
+ import numpy as np
8
+ import matplotlib.pyplot as plt
9
+ from tslearn.metrics import dtw_path
10
+ import scipy.stats as stats
11
+
12
+ # %% Global constants
13
+ TITLE_FONTSIZE = 20
14
+ SUPLABEL_FONTSIZE = 15
15
+ INFO_TXT_FILENAME = "CorrelateAnalysis_Info.txt"
16
+
17
+
18
+ # %% Main function
19
+ def timeseries_correlation(
20
+ series_1, series_2, name_1, name_2, savepath, num_boots, outlier, try_to_fix_ylims
21
+ ):
22
+ """
23
+ Conceptual Approach
24
+ -------------------
25
+ To assess latency correlations between 2 time series:
26
+ 1) Get Single Subject (or group) time series
27
+ 2) Get N bootstrap time series (w. same bootstrapping
28
+ subject-distributions for the two time series)
29
+ 3) DTW of 2) with 1) using AREA measure!
30
+ 4) Gives you two numbers for each bootstrap sample -> DTW result of
31
+ time series 1 & 2 latency (lag)
32
+ 5) Correlate these with Pearson and Spearman (latter preferred for
33
+ presentations)
34
+
35
+ Input Parameters
36
+ ----------------
37
+ 1) series_1, series_2
38
+ => Timeseries as two matrices of shape:
39
+ dataindices (e.g., timepoints) x subjects (or repetitions)
40
+ 2) name_1, name_2
41
+ => Names of your timeseries
42
+ 3) savepath
43
+ => Where to save output to
44
+ 4) num_boots
45
+ => Number of bootstraps (we recommend at least 10000)
46
+ 5) outlier
47
+ => 1: to exclude +-5SD outliers from your scatterplot
48
+ => 0: don't exclude outliers
49
+ 6) try_to_fix_ylims
50
+ => 1: try to fix ylimits of marginals and include lines for easier
51
+ comparison
52
+ - based on num_boots
53
+ - if you want to modify this yourself (e.g., hardcode lines &
54
+ maxima, change the if statement blocks for the two marginals)
55
+ => 0: let matlab handle ylimits of marginals (likely won't allow
56
+ comparisons
57
+
58
+ Note
59
+ ----
60
+ 1) Number of subjects has to be equal in both time series!
61
+ 2) Your full time series are analysed.
62
+ If you would like to assess only a specific interval of your time
63
+ series, use indexing before calling this function.
64
+ 3) We fix marginals' x-axis ranges to +-8 SD. This should be okay for
65
+ most data-sets since we zscore DTW area distributions. However, if you
66
+ do not want to standardise like this (which will make your marginals
67
+ harder to compare) or you want to implement different values, change
68
+ the lines that use xlimits & ylimits (see line 180)
69
+ """
70
+
71
+ # %% Preparation
72
+
73
+ # first check if user tried to compare groups with unequal sample sizes
74
+ if np.shape(series_1)[1] != np.shape(series_2)[1]:
75
+ print("Your time series have a different number of subjects - cancelling!")
76
+ return
77
+
78
+ # prepare some vars
79
+ name_1 = str(name_1)
80
+ name_2 = str(name_2)
81
+ plotpath = os.path.join(savepath, "Plots/")
82
+ varpath = os.path.join(savepath, "Variables/")
83
+ if not os.path.exists(plotpath):
84
+ os.makedirs(plotpath)
85
+ if not os.path.exists(varpath):
86
+ os.makedirs(varpath)
87
+ DTW_marginals = {"name_1": [], "name_2": []}
88
+ DTW_stats = dict()
89
+
90
+ # %% Run the analysis
91
+ series_1_dtw_area, series_2_dtw_area = run_bootstrap_dtw(
92
+ series_1, series_2, num_boots
93
+ )
94
+
95
+ # %% Outlier Rejection
96
+ if outlier:
97
+
98
+ # exclude outliers if +-5 SD (change in std_thresh line if wanted)
99
+ series_1_std = np.std(series_1_dtw_area)
100
+ series_2_std = np.std(series_2_dtw_area)
101
+ std_thresh = 5 # Threshold for outlier rejection
102
+
103
+ # *****************************************************************
104
+ # IMPORTANT
105
+ # This might seem like a strange way of removing outliers
106
+ # ==> However it ensures that we check all 4 directions of the
107
+ # scatterplot for outliers, save indices as we do so, then just
108
+ # take those unique indices (removing duplicates) and (IMPORTANTLY)
109
+ # IN THE END removing these indices FROM BOTH DTW distributions
110
+ # ==> THIS IS IMPORTANT BECAUSE DTW DISTRIBUTIONS HAVE TO STAY
111
+ # LINKED DUE TO THE BOOTSTRAP
112
+ # *****************************************************************
113
+
114
+ outlier_idx = []
115
+ # series 1 - positive outliers
116
+ series_1_pos_outlier_idx = np.where(
117
+ series_1_dtw_area
118
+ > (np.mean(series_1_dtw_area) + (std_thresh * series_1_std))
119
+ )[0]
120
+ if len(series_1_pos_outlier_idx) > 0:
121
+ outlier_idx.extend(series_1_pos_outlier_idx)
122
+ # series 1 - negative outliers
123
+ series_1_neg_outlier_idx = np.where(
124
+ series_1_dtw_area
125
+ < (np.mean(series_1_dtw_area) - (std_thresh * series_1_std))
126
+ )[0]
127
+ if len(series_1_neg_outlier_idx) > 0:
128
+ outlier_idx.extend(series_1_neg_outlier_idx)
129
+ # series 2 - positive outliers
130
+ series_2_pos_outlier_idx = np.where(
131
+ series_2_dtw_area
132
+ > (np.mean(series_2_dtw_area) + (std_thresh * series_2_std))
133
+ )[0]
134
+ if len(series_2_pos_outlier_idx) > 0:
135
+ outlier_idx.extend(series_2_pos_outlier_idx)
136
+ # series 2 - negative outliers
137
+ series_2_neg_outlier_idx = np.where(
138
+ series_2_dtw_area
139
+ < (np.mean(series_2_dtw_area) - (std_thresh * series_2_std))
140
+ )[0]
141
+ if len(series_2_neg_outlier_idx) > 0:
142
+ outlier_idx.extend(series_1_neg_outlier_idx)
143
+
144
+ # remove duplicates
145
+ outlier_idx = np.unique(outlier_idx)
146
+
147
+ # remove outliers from BOTH series!
148
+ if len(outlier_idx) > 0:
149
+ series_1_dtw_area = np.delete(series_1_dtw_area, outlier_idx)
150
+ series_2_dtw_area = np.delete(series_2_dtw_area, outlier_idx)
151
+
152
+ # %% Save var, skew & kurtosis values of DTW Area Distributions
153
+ DTW_stats["series_1_var"] = np.var(series_1_dtw_area)
154
+ DTW_stats["series_1_skew"] = stats.skew(series_1_dtw_area)
155
+ DTW_stats["series_1_kurt"] = stats.kurtosis(series_1_dtw_area)
156
+ DTW_stats["series_2_var"] = np.var(series_2_dtw_area)
157
+ DTW_stats["series_2_skew"] = stats.skew(series_2_dtw_area)
158
+ DTW_stats["series_2_kurt"] = stats.kurtosis(series_2_dtw_area)
159
+ if outlier:
160
+ picklepath = os.path.join(
161
+ varpath, ("%iSD OutlierRejected Marginal Stats.pkl" % (std_thresh))
162
+ )
163
+ with open(picklepath, "wb") as file:
164
+ pickle.dump(DTW_stats, file)
165
+ else:
166
+ picklepath = os.path.join(varpath, ("Marginal Stats.pkl"))
167
+ with open(picklepath, "wb") as file:
168
+ pickle.dump(DTW_stats, file)
169
+
170
+ # %% Standardise DTW Area Distributions & correlate
171
+ # So line of linear fit in scatter & marginal distributions reflect correlation
172
+ # values better (since correlations have internal standardisation)
173
+
174
+ # zscore
175
+ z_x = stats.zscore(series_1_dtw_area)
176
+ z_y = stats.zscore(series_2_dtw_area)
177
+
178
+ # pearson corr
179
+ pears_res = stats.pearsonr(z_x, z_y)
180
+ pears_r = pears_res[0]
181
+ pears_p = pears_res[1]
182
+
183
+ # spearman corr
184
+ spear_res = stats.spearmanr(z_x, z_y)
185
+ spear_r = spear_res[0]
186
+ spear_p = spear_res[1]
187
+
188
+ # prepare strings
189
+ str_df = str(num_boots - 2) # degrees of freedom
190
+ pears_str_p = ""
191
+ spear_str_p = ""
192
+ if pears_p < 0.0001:
193
+ pears_str_p = "p < .0001"
194
+ else:
195
+ pears_str_p = "p = " + str(pears_p.round(3))
196
+ if spear_p < 0.0001:
197
+ spear_str_p = "p < .0001"
198
+ else:
199
+ spear_str_p = "p = " + str(spear_p.round(3))
200
+
201
+ # %% Plot one large figure
202
+ f, ax_global = plt.subplots()
203
+ ax_global.set_xticks([])
204
+ ax_global.set_yticks([])
205
+ for spine in ax_global.spines.values(): # despine outmost axis
206
+ spine.set_visible(False)
207
+ gs = f.add_gridspec(4, 4, wspace=0, hspace=0) # prepare the grid
208
+ scatter_color = "#c79fef" # lavender
209
+ line_color = "#8ab8fe" # carolina blue
210
+ xlimits = [-8, 8]
211
+ xticks = np.squeeze([-8, -4, 0, 4, 8])
212
+ ylimits = [-8, 8]
213
+ yticks = xticks.copy()
214
+ if try_to_fix_ylims:
215
+ ylimmax = num_boots / 8
216
+ ref_val_1 = round(ylimmax * 0.25)
217
+ ref_val_2 = round(ylimmax * 0.5)
218
+ ref_val_3 = round(ylimmax * 0.75)
219
+
220
+ # %% first, scatterplot
221
+ ax = f.add_subplot(gs[0:3, 1:]) # set wanted gridcells as ax
222
+ ax.scatter(
223
+ z_x, z_y, c="white", edgecolor=scatter_color, s=7, linewidth=0.5, alpha=0.6
224
+ )
225
+ ax.set_ylim(ylimits)
226
+ ax.set_xlim(xlimits)
227
+ ax.set_xticks(xticks)
228
+ ax.set_yticks(yticks)
229
+ ax.set_xticklabels([])
230
+ ax.set_yticklabels([])
231
+ series_1_xlimits = ax.get_xlim() # for plotting marginals later
232
+ series_1_xticks = ax.get_xticks()
233
+ series_2_xlimits = ax.get_ylim()
234
+ series_2_xticks = ax.get_yticks()
235
+ for spine in ax.spines.values():
236
+ spine.set_visible(False)
237
+ # ax.set_title("DTW area-diff correlations of %s & %s.\nPearson: r(%s) = %s: %s & Spearman: r(%s) = %s: %s" % (name_1, name_2, str_df, pears_str_r, pears_str_p, str_df, spear_str_r, spear_str_p))
238
+ ax.set_title(
239
+ "DTW area-diff correlations of "
240
+ + name_1
241
+ + " & "
242
+ + name_2
243
+ + "\nPearson: r("
244
+ + str(str_df)
245
+ + ") = "
246
+ + str(pears_r.round(2))
247
+ + ": "
248
+ + pears_str_p
249
+ + "\nSpearman: r("
250
+ + str(str_df)
251
+ + ") = "
252
+ + str(spear_r.round(2))
253
+ + ": "
254
+ + spear_str_p
255
+ )
256
+ # have to create the line of least squares manually
257
+ coefficients = np.polyfit(z_x, z_y, 1) # 1 == linear (fit coefficients to orig z_x)
258
+ slope, intercept = coefficients
259
+ # create y_fit using new z_x that spans over the full xlimits (so line isnt cut-off)
260
+ z_x_new = np.arange(xlimits[0], xlimits[1] + 1, 1)
261
+ y_fit = intercept + (slope * z_x_new)
262
+ ax.plot(z_x_new, y_fit, linewidth=1.5, color=line_color)
263
+
264
+ # %% second, series_2 marginal distribution
265
+ ax_series_2 = f.add_subplot(gs[:-1, 0])
266
+ ax_series_2.hist(
267
+ z_y,
268
+ 100,
269
+ orientation="horizontal",
270
+ facecolor=scatter_color,
271
+ edgecolor=line_color,
272
+ linewidth=0.15,
273
+ )
274
+ ax_series_2.set_ylim(series_2_xlimits) # set ylim because horizontal hist!
275
+ ax_series_2.set_yticks(series_2_xticks)
276
+ ax_series_2.invert_xaxis()
277
+ ax_series_2.set_ylabel(name_2 + " Marginal Distribution")
278
+ for side in ax_series_2.spines.keys(): # side is left/right/top/bottom
279
+ ax_series_2.spines[side].set_linewidth(0.2)
280
+ if side == "top":
281
+ ax_series_2.spines[side].set_visible(False)
282
+ if try_to_fix_ylims:
283
+ ax_series_2.set_xlim(left=ylimmax)
284
+ ref_val_strings = []
285
+ for val in [ref_val_1, ref_val_2, ref_val_3]:
286
+ ax_series_2.axvline(val, color="#029386", linewidth=0.2, linestyle="--")
287
+ ref_val_strings.append(str(val))
288
+ ax_series_2.set_xticks(
289
+ [ref_val_1, ref_val_2, ref_val_3], labels=ref_val_strings
290
+ )
291
+
292
+ # %% third, series_1 marginal distribution
293
+ ax_series_1 = f.add_subplot(gs[-1, 1:])
294
+ ax_series_1.hist(
295
+ z_x, 100, facecolor=scatter_color, edgecolor=line_color, linewidth=0.15
296
+ )
297
+ ax_series_1.set_xlim(series_1_xlimits)
298
+ ax_series_1.set_xticks(series_1_xticks)
299
+ ax_series_1.set_xlabel(name_1 + " Marginal Distribution")
300
+ for side in ax_series_1.spines.keys(): # side is left/right/top/bottom
301
+ ax_series_1.spines[side].set_linewidth(0.2)
302
+ if side == "left":
303
+ ax_series_1.spines[side].set_visible(False)
304
+ if try_to_fix_ylims:
305
+ ax_series_1.set_ylim(top=ylimmax)
306
+ ref_val_strings = []
307
+ for val in [ref_val_1, ref_val_2, ref_val_3]:
308
+ ax_series_1.axhline(val, color="#029386", linewidth=0.2, linestyle="--")
309
+ ref_val_strings.append(str(val))
310
+ ax_series_1.set_yticks(
311
+ [ref_val_1, ref_val_2, ref_val_3], labels=ref_val_strings
312
+ )
313
+ ax_series_1.yaxis.set_ticks_position("right")
314
+
315
+ # %% save figures & marginal distributions
316
+ # figures
317
+ if outlier:
318
+ fig_filename = (
319
+ "DTW Latency Correlation - "
320
+ + name_1
321
+ + " & "
322
+ + name_2
323
+ + " - "
324
+ + str(std_thresh)
325
+ + "std outlier removed"
326
+ )
327
+ marginals_filename = (
328
+ "Marginal Distributions - " + str(std_thresh) + "std outlier removed"
329
+ )
330
+ else:
331
+ fig_filename = "DTW Latency Correlation - " + name_1 + " & " + name_2
332
+ marginals_filename = "Marginal Distributions"
333
+ f.savefig(
334
+ os.path.join(plotpath, fig_filename + ".png"),
335
+ bbox_inches="tight",
336
+ dpi=300,
337
+ )
338
+ f.savefig(
339
+ os.path.join(plotpath, fig_filename + ".svg"),
340
+ bbox_inches="tight",
341
+ dpi=300,
342
+ )
343
+ # marginal distributions
344
+ series_2_marginals = series_2_dtw_area
345
+ series_1_marginals = series_1_dtw_area
346
+ z_series_2_marginals = z_y
347
+ z_series_1_marginals = z_x
348
+ vars_to_save = {
349
+ "series_1_marginals": series_1_marginals,
350
+ "series_2_marginals": series_2_marginals,
351
+ "z_series_1_marginals": z_series_1_marginals,
352
+ "z_series_2_marginals": z_series_2_marginals,
353
+ }
354
+
355
+ picklepath = os.path.join(varpath, (marginals_filename + ".pkl"))
356
+ with open(picklepath, "wb") as file:
357
+ pickle.dump(vars_to_save, file)
358
+ plt.show(block=False) # otherwise stopped further code if python via terminal
359
+ plt.pause(0.001)
360
+
361
+
362
+ # %% Local functions
363
+ def run_bootstrap_dtw(series_1, series_2, num_boots):
364
+ """Run the main bootstrap DTW analysis"""
365
+ # compute standardised (subjects / reps) averages of both time series
366
+ avg_series_1 = series_1.mean(axis=1)
367
+ avg_series_2 = series_2.mean(axis=1)
368
+ z_avg_series_1 = stats.zscore(avg_series_1)
369
+ z_avg_series_2 = stats.zscore(avg_series_2)
370
+
371
+ # bootstrap manually using randint & run dtw between bootstrap & observed z_avg
372
+ num_subs = np.shape(series_1)[1]
373
+ series_1_dtw_area = np.zeros(num_boots)
374
+ series_2_dtw_area = np.zeros(num_boots)
375
+
376
+ # bootstrap loop
377
+ for i in range(num_boots):
378
+ print("\nboots num #%s" % (i))
379
+
380
+ # get num_subj bootstrap samples (using randint == we bootstrap with duplicates)
381
+ this_trap = np.random.randint(0, num_subs, num_subs)
382
+ boot_series_1 = series_1[:, this_trap]
383
+ boot_series_2 = series_2[:, this_trap]
384
+
385
+ # average & standardise trapped series
386
+ avg_boot_series_1 = boot_series_1.mean(axis=1)
387
+ avg_boot_series_2 = boot_series_2.mean(axis=1)
388
+ z_avg_boot_series_1 = stats.zscore(avg_boot_series_1)
389
+ z_avg_boot_series_2 = stats.zscore(avg_boot_series_2)
390
+
391
+ # ***** dtw between z-scored averages of series & bootstrapped series *****
392
+
393
+ # series_1
394
+ WP_1, score_1 = dtw_path(z_avg_boot_series_1, z_avg_series_1)
395
+ ix_1 = []
396
+ iy_1 = []
397
+ for WP_idxs in WP_1:
398
+ ix_1.append(WP_idxs[0])
399
+ iy_1.append(WP_idxs[1])
400
+ xmax_1 = ix_1[-1] + 1 # because this is used with range
401
+ DIAG_1 = np.arange(xmax_1)
402
+ areaDIAG_1 = np.trapz(DIAG_1)
403
+ # IMPORTANT NOTE ABOUT NP.TRAPZ - TAKES IN (Y) & (Y,X) WHEREAS MATLAB'S TRAPZ
404
+ # TAKES (Y) & (X,Y) !!!!!
405
+ areaWP_1 = np.trapz(iy_1, ix_1)
406
+ series_1_dtw_area[i] = (areaDIAG_1 - areaWP_1) / areaDIAG_1
407
+
408
+ # series_2
409
+ WP_2, score_2 = dtw_path(z_avg_boot_series_2, z_avg_series_2)
410
+ ix_2 = []
411
+ iy_2 = []
412
+ for WP_idxs in WP_2:
413
+ ix_2.append(WP_idxs[0])
414
+ iy_2.append(WP_idxs[1])
415
+ xmax_2 = ix_2[-1] + 1 # because this is used with range
416
+ DIAG_2 = np.arange(xmax_2)
417
+ areaDIAG_2 = np.trapz(DIAG_2)
418
+ # IMPORTANT NOTE ABOUT NP.TRAPZ - TAKES IN (Y) & (Y,X) WHEREAS MATLAB'S TRAPZ
419
+ # TAKES (Y) & (X,Y) !!!!!
420
+ areaWP_2 = np.trapz(iy_2, ix_2)
421
+ series_2_dtw_area[i] = (areaDIAG_2 - areaWP_2) / areaDIAG_2
422
+
423
+ # bootstrap loop end
424
+ return series_1_dtw_area, series_2_dtw_area