fmatoolbox 0.1.0__tar.gz → 0.1.2__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.
- {fmatoolbox-0.1.0/src/fmatoolbox.egg-info → fmatoolbox-0.1.2}/PKG-INFO +2 -2
- {fmatoolbox-0.1.0 → fmatoolbox-0.1.2}/pyproject.toml +2 -2
- {fmatoolbox-0.1.0 → fmatoolbox-0.1.2}/src/fmatoolbox/analysis.py +137 -63
- {fmatoolbox-0.1.0 → fmatoolbox-0.1.2}/src/fmatoolbox/general.py +104 -55
- {fmatoolbox-0.1.0 → fmatoolbox-0.1.2}/src/fmatoolbox/plotting.py +129 -72
- {fmatoolbox-0.1.0 → fmatoolbox-0.1.2}/src/fmatoolbox/regions.py +36 -24
- {fmatoolbox-0.1.0 → fmatoolbox-0.1.2/src/fmatoolbox.egg-info}/PKG-INFO +2 -2
- {fmatoolbox-0.1.0 → fmatoolbox-0.1.2}/LICENSE +0 -0
- {fmatoolbox-0.1.0 → fmatoolbox-0.1.2}/README.md +0 -0
- {fmatoolbox-0.1.0 → fmatoolbox-0.1.2}/setup.cfg +0 -0
- {fmatoolbox-0.1.0 → fmatoolbox-0.1.2}/src/fmatoolbox/__init__.py +0 -0
- {fmatoolbox-0.1.0 → fmatoolbox-0.1.2}/src/fmatoolbox/data.py +0 -0
- {fmatoolbox-0.1.0 → fmatoolbox-0.1.2}/src/fmatoolbox/exceptions.py +0 -0
- {fmatoolbox-0.1.0 → fmatoolbox-0.1.2}/src/fmatoolbox.egg-info/SOURCES.txt +0 -0
- {fmatoolbox-0.1.0 → fmatoolbox-0.1.2}/src/fmatoolbox.egg-info/dependency_links.txt +0 -0
- {fmatoolbox-0.1.0 → fmatoolbox-0.1.2}/src/fmatoolbox.egg-info/requires.txt +0 -0
- {fmatoolbox-0.1.0 → fmatoolbox-0.1.2}/src/fmatoolbox.egg-info/top_level.txt +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fmatoolbox
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Python port of FMAToolbox
|
|
5
5
|
Author: Pietro Bozzo
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
7
7
|
Project-URL: Homepage, https://github.com/pietro-bozzo/FMAToolboxPython
|
|
8
8
|
Project-URL: Repository, https://github.com/pietro-bozzo/FMAToolboxPython
|
|
9
9
|
Project-URL: Issues, https://github.com/pietro-bozzo/FMAToolboxPython/issues
|
|
10
|
-
Requires-Python: >=3.
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE
|
|
13
13
|
Requires-Dist: numpy
|
|
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fmatoolbox"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "Python port of FMAToolbox"
|
|
9
9
|
authors = [{name = "Pietro Bozzo"}]
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
license = "GPL-3.0-or-later"
|
|
12
12
|
|
|
13
|
-
requires-python = ">=3.
|
|
13
|
+
requires-python = ">=3.11"
|
|
14
14
|
dependencies = [
|
|
15
15
|
"numpy",
|
|
16
16
|
"scipy",
|
|
@@ -7,9 +7,86 @@ import sklearn.decomposition as skdc
|
|
|
7
7
|
import skimage.filters as skif
|
|
8
8
|
import joblib
|
|
9
9
|
import statsmodels.stats.multitest
|
|
10
|
+
import warnings
|
|
10
11
|
from typing import Callable
|
|
11
12
|
|
|
12
13
|
|
|
14
|
+
def istantaneousRate(samples, start:float=None, stop:float=None, bin:float=None, step:int=None, smooth:float=None, g_range:tuple[int,int]=None):
|
|
15
|
+
'''
|
|
16
|
+
estimate the istantaneous rate of a point process from a realization of its time stamps, e.g., the firing rate from spike times
|
|
17
|
+
|
|
18
|
+
arguments:
|
|
19
|
+
samples (n,:) float, every row is either [sample time (s)] or [sample time (s), process id]; if 1d, interpreted as (n,1)
|
|
20
|
+
start float = min(samples[:,0]) s, time to start count at
|
|
21
|
+
stop float = max(samples[:,0]) s, time to stop count at
|
|
22
|
+
bin float = 0.05 s, time bin to count samples
|
|
23
|
+
step int = 1, rate is computed in windows of length 'bin' and overlap 'bin' / 'step', default is no overlap
|
|
24
|
+
smooth float = None, gaussian kernel std to smooth rate over time
|
|
25
|
+
g_range (2) int, range of groups (process ids) to consider, default is [min(samples[:,1]), max(samples[:,1])]
|
|
26
|
+
(boundaries included, only for 2-columns 'samples')
|
|
27
|
+
|
|
28
|
+
output:
|
|
29
|
+
rate (:,g+1) float, every row is [time stamp, rates for g processes], g is 1 if 'samples' has just one column
|
|
30
|
+
'''
|
|
31
|
+
|
|
32
|
+
# validate input
|
|
33
|
+
samples = np.asarray(samples)
|
|
34
|
+
if bin is None: bin = 0.05
|
|
35
|
+
if step is None: step = 1
|
|
36
|
+
if step % 1 or step == 0:
|
|
37
|
+
raise ValueError("'step' must be a non-zero integer")
|
|
38
|
+
|
|
39
|
+
groups = []
|
|
40
|
+
if samples.ndim == 1:
|
|
41
|
+
times = samples
|
|
42
|
+
elif samples.shape[1] == 1:
|
|
43
|
+
times = samples.reshape(-1)
|
|
44
|
+
else:
|
|
45
|
+
times = samples[:,0]
|
|
46
|
+
groups = samples[:,1]
|
|
47
|
+
if g_range is None:
|
|
48
|
+
g_range = np.unique(groups).astype(int)[[0,-1]]
|
|
49
|
+
else:
|
|
50
|
+
g_range = np.array(g_range,dtype=int)
|
|
51
|
+
if start is None: start = times.min()
|
|
52
|
+
if stop is None: stop = times.max()
|
|
53
|
+
|
|
54
|
+
def histogramVectorized(x,nbins):
|
|
55
|
+
a, b = x.shape
|
|
56
|
+
# assign bin indeces
|
|
57
|
+
bin_idx = (x / bin).astype(int) # bin_idx = 0, bin_idx = n_bins + 1 after 'stop'
|
|
58
|
+
bin_idx = np.clip(bin_idx,0,nbins-1)
|
|
59
|
+
offset = np.arange(b)[None,:] * nbins
|
|
60
|
+
hist = np.bincount((bin_idx+offset).ravel(), minlength=nbins*b).reshape(b,nbins).T
|
|
61
|
+
return hist
|
|
62
|
+
|
|
63
|
+
# repeat 'times' by 'step' times
|
|
64
|
+
times = np.tile(times[:,None],(1,step)) # (times, step)
|
|
65
|
+
# shift by: 'start', 'bin' to add bin before 'start', overlap for each repetition
|
|
66
|
+
times = times - start + bin - np.arange(step)*(bin/step)
|
|
67
|
+
# parameters
|
|
68
|
+
n_bins = int((stop + bin - start) // bin)
|
|
69
|
+
n_bins2 = n_bins + 2 # add two bins: first for 'samples' before 'start', last for 'samples' after 'stop'
|
|
70
|
+
if len(groups) == 0:
|
|
71
|
+
hist = histogramVectorized(times,n_bins2)
|
|
72
|
+
rate = (hist[1:-1] / bin).ravel() # remove data outside [start, stop], convert to rate (Hz), flatten
|
|
73
|
+
else:
|
|
74
|
+
rate = []
|
|
75
|
+
for g in range(g_range[0],g_range[-1] + 1):
|
|
76
|
+
hist = histogramVectorized(times[groups==g],n_bins2)
|
|
77
|
+
rate.append((hist[1:-1]).ravel())
|
|
78
|
+
rate = np.array(rate).T / bin
|
|
79
|
+
|
|
80
|
+
# make time axis
|
|
81
|
+
t = bin/step/2 + np.linspace(0,(n_bins-1)*bin,n_bins*step)
|
|
82
|
+
|
|
83
|
+
# apply smoothing
|
|
84
|
+
if smooth is not None:
|
|
85
|
+
firing_rate = sp.ndimage.gaussian_filter(firing_rate,smooth,axes=0)
|
|
86
|
+
|
|
87
|
+
return np.column_stack((t,rate))
|
|
88
|
+
|
|
89
|
+
|
|
13
90
|
def firingRate(spikes, start:float=None, stop:float=None, bin_size:float=None, step:int=None, smooth:float=None, u_range:tuple[int,int]=None):
|
|
14
91
|
# estimate istantaneous firing rate from spike times
|
|
15
92
|
#
|
|
@@ -26,6 +103,8 @@ def firingRate(spikes, start:float=None, stop:float=None, bin_size:float=None, s
|
|
|
26
103
|
# output:
|
|
27
104
|
# firing_rate (:,m+1) float, every row is [time stamp, firing rates for m units], m is 1 if spikes has just one column
|
|
28
105
|
|
|
106
|
+
warnings.warn("firingRate is deprecated, use istantaneousRate instead!", DeprecationWarning)
|
|
107
|
+
|
|
29
108
|
# validate input
|
|
30
109
|
spikes = np.asarray(spikes)
|
|
31
110
|
if bin_size is None: bin_size = 0.05
|
|
@@ -343,41 +422,35 @@ def reactivationStrength(raster, templates, threshold:float=5):
|
|
|
343
422
|
|
|
344
423
|
# --- statistics functions ---
|
|
345
424
|
|
|
346
|
-
def MCpValue(surrogate,
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
Returns
|
|
360
|
-
-------
|
|
361
|
-
pvals : ndarray, shape (n_features,)
|
|
362
|
-
Monte Carlo p-values
|
|
363
|
-
"""
|
|
425
|
+
def MCpValue(surrogate,observed,alternative='two-sided'):
|
|
426
|
+
'''
|
|
427
|
+
compute Monte Carlo p-values comparing observed statistics to surrogate distributions
|
|
428
|
+
|
|
429
|
+
arguments:
|
|
430
|
+
surrogate (s,f) float, surrogate statistics; s: # surrogates, f: # features
|
|
431
|
+
observed (f,) float, observed statistics
|
|
432
|
+
alternative str = {"two-sided", "greater", "less"}, test direction
|
|
433
|
+
|
|
434
|
+
output:
|
|
435
|
+
pvals (f,) float, Monte Carlo p-values
|
|
436
|
+
'''
|
|
364
437
|
|
|
365
438
|
surrogate = np.asarray(surrogate)
|
|
366
|
-
|
|
439
|
+
observed = np.asarray(observed).ravel()
|
|
367
440
|
if surrogate.ndim == 1:
|
|
368
441
|
surrogate = surrogate.reshape((-1,1))
|
|
369
|
-
if surrogate.shape[1] !=
|
|
370
|
-
raise ValueError("
|
|
442
|
+
if surrogate.shape[1] != observed.shape[0]:
|
|
443
|
+
raise ValueError("'observed' must have one element for every column of 'surrogates'")
|
|
371
444
|
|
|
372
445
|
if alternative == "greater":
|
|
373
|
-
count = np.sum(surrogate >=
|
|
446
|
+
count = np.sum(surrogate >= observed, axis=0)
|
|
374
447
|
|
|
375
448
|
elif alternative == "less":
|
|
376
|
-
count = np.sum(surrogate <=
|
|
449
|
+
count = np.sum(surrogate <= observed, axis=0)
|
|
377
450
|
|
|
378
451
|
elif alternative == "two-sided":
|
|
379
|
-
greater = np.sum(surrogate >=
|
|
380
|
-
less = np.sum(surrogate <=
|
|
452
|
+
greater = np.sum(surrogate >= observed, axis=0)
|
|
453
|
+
less = np.sum(surrogate <= observed, axis=0)
|
|
381
454
|
count = 2 * np.minimum(greater, less)
|
|
382
455
|
|
|
383
456
|
else:
|
|
@@ -388,26 +461,19 @@ def MCpValue(surrogate,real,alternative='two-sided'):
|
|
|
388
461
|
return np.minimum(pvals, 1.0)
|
|
389
462
|
|
|
390
463
|
|
|
391
|
-
def holmBonferroni(pvals,alpha=0.05,return_reject=False):
|
|
392
|
-
|
|
464
|
+
def holmBonferroni(pvals, alpha:float=0.05, return_reject:bool=False):
|
|
465
|
+
'''
|
|
393
466
|
Holm-Bonferroni correction for multiple tests
|
|
394
467
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
Returns
|
|
405
|
-
-------
|
|
406
|
-
corrected : ndarray
|
|
407
|
-
adjusted p-values, preserves input shape
|
|
408
|
-
reject : ndarray of bool, optional
|
|
409
|
-
true for hypothesis that can be rejected for given alpha, preserves input shape
|
|
410
|
-
"""
|
|
468
|
+
arguments:
|
|
469
|
+
pvals (n,) float, p values, NaNs are ignored in the correction procedure and propagated in output
|
|
470
|
+
alpha float = 0.05, significance level, must be in [0,1]
|
|
471
|
+
return_reject bool = False, return also rejection decisions
|
|
472
|
+
|
|
473
|
+
output:
|
|
474
|
+
corrected (n,) float, adjusted p values
|
|
475
|
+
reject (n,) bool, optional, true for hypothesis that can be rejected
|
|
476
|
+
'''
|
|
411
477
|
|
|
412
478
|
pvals = np.asarray(pvals)
|
|
413
479
|
original_shape = pvals.shape
|
|
@@ -430,45 +496,53 @@ def holmBonferroni(pvals,alpha=0.05,return_reject=False):
|
|
|
430
496
|
return corrected
|
|
431
497
|
|
|
432
498
|
|
|
433
|
-
def maxStatisticTest(data, surrogate, statistic=None, alpha:float=0.05, alternative:str='two-sided'):
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
499
|
+
def maxStatisticTest(data, surrogate, statistic=None, group=None, alpha:float=0.05, alternative:str='two-sided'):
|
|
500
|
+
'''
|
|
501
|
+
conduct a max statistic test over time, assessing in which time points the null hypothesis about a statistic across sessions can be rejected
|
|
502
|
+
|
|
503
|
+
arguments:
|
|
504
|
+
data (sessions, times) float
|
|
505
|
+
surrogate (sessions, times, surrogates) float
|
|
506
|
+
group (sessions,) int, grouping variable used to aggregate sessions, the statistic is computed per group and then again over groups
|
|
507
|
+
alpha float = 0.05, significance level, must be in [0,1]
|
|
508
|
+
alternative str = {'two-sided','grater','less'}, test direction, defines the null hypothesis
|
|
509
|
+
'''
|
|
510
|
+
|
|
440
511
|
data = np.array(data,ndmin=2)
|
|
441
|
-
surrogate = np.array(surrogate,ndmin=
|
|
512
|
+
surrogate = np.array(surrogate,ndmin=3)
|
|
442
513
|
if data.shape[:2] != surrogate.shape[:2]:
|
|
443
|
-
raise ValueError("'data' and 'surrogate' must have the same first two dimensions")
|
|
444
|
-
if surrogate.ndim != 3:
|
|
445
|
-
raise ValueError("'surrogate' must have dimensions (sessions, times, surrogates)")
|
|
514
|
+
raise ValueError("'data' and 'surrogate' must have the same first two dimensions (sessions, times)")
|
|
446
515
|
if statistic is None:
|
|
447
516
|
statistic = lambda x : np.nanmean(x,axis=0)
|
|
448
517
|
n_times = data.shape[1]
|
|
449
518
|
n_surrogates = surrogate.shape[2]
|
|
450
519
|
|
|
451
520
|
# statistic for real and surrogate data
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
521
|
+
if group is None:
|
|
522
|
+
s_real = statistic(data) # (times,)
|
|
523
|
+
s_surrogate = statistic(surrogate) # (times, surrogates)
|
|
524
|
+
else:
|
|
525
|
+
unique_groups = np.unique(group)
|
|
526
|
+
s_real = [statistic(data[group==g]) for g in unique_groups]
|
|
527
|
+
s_real = statistic(s_real)
|
|
528
|
+
s_surrogate = [statistic(surrogate[group==g]) for g in unique_groups]
|
|
529
|
+
s_surrogate = statistic(s_surrogate)
|
|
456
530
|
|
|
457
531
|
# p-values per time point
|
|
458
532
|
if alternative == 'greater':
|
|
459
|
-
s_surrogate = np.min(s_surrogate,axis=0) # (
|
|
460
|
-
p = MCpValue(np.tile(s_surrogate,(n_times,1)).T,s_real,alternative) # (
|
|
533
|
+
s_surrogate = np.min(s_surrogate,axis=0) # (surrogates,)
|
|
534
|
+
p = MCpValue(np.tile(s_surrogate,(n_times,1)).T,s_real,alternative) # (times,)
|
|
461
535
|
elif alternative == 'less':
|
|
462
536
|
s_surrogate = np.max(s_surrogate,axis=0)
|
|
463
537
|
p = MCpValue(np.tile(s_surrogate,(n_times,1)).T,s_real,alternative)
|
|
464
538
|
elif alternative == 'two-sided':
|
|
465
539
|
# standardize statistic to ensure proper two-tailed test
|
|
466
|
-
mu = np.mean(s_surrogate,axis=1) # (
|
|
540
|
+
mu = np.mean(s_surrogate,axis=1) # (times,)
|
|
467
541
|
sigma = np.std(s_surrogate,axis=1,ddof=1)
|
|
468
542
|
s_real = np.abs((s_real - mu) / sigma) # abs(z-score( ))
|
|
469
543
|
s_surrogate = (s_surrogate - mu.reshape(-1,1)) / sigma.reshape(-1,1)
|
|
470
|
-
s_surrogate = np.max(np.abs(s_surrogate),axis=0) # max_t(abs(z-score( ))), i.e., (
|
|
471
|
-
p = MCpValue(np.tile(s_surrogate,(n_times,1)).T,s_real,
|
|
544
|
+
s_surrogate = np.max(np.abs(s_surrogate),axis=0) # max_t(abs(z-score( ))), i.e., (surrogates,)
|
|
545
|
+
p = MCpValue(np.tile(s_surrogate,(n_times,1)).T,s_real,'greater')
|
|
472
546
|
else:
|
|
473
547
|
raise ValueError("'alternative' must be 'two-sided', 'greater' or 'less'")
|
|
474
548
|
|
|
@@ -16,10 +16,7 @@ def consolidateIntervals(intervals, epsilon:float=0, duration:float=0):
|
|
|
16
16
|
# output:
|
|
17
17
|
# intervals (:,2) float, consolidated intervals (s)
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
intervals = np.array(intervals,dtype=float,ndmin=2)
|
|
21
|
-
except Exception as e:
|
|
22
|
-
raise TypeError("'intervals' must be convertible to a NumPy array") from e
|
|
19
|
+
intervals = np.array(intervals,dtype=float,ndmin=2)
|
|
23
20
|
if intervals.shape[1] != 2:
|
|
24
21
|
raise ValueError("'intervals' must be a (n,2) array")
|
|
25
22
|
if (intervals[:,0] > intervals[:,1]).any():
|
|
@@ -143,7 +140,7 @@ def intersectIntervals(intervals):
|
|
|
143
140
|
return intersection
|
|
144
141
|
|
|
145
142
|
|
|
146
|
-
def restrict(samples,intervals,shift=False,s_ind=False,i_ind=False):
|
|
143
|
+
def restrict(samples, intervals, shift:bool=False, s_ind:bool=False, i_ind:bool=False):
|
|
147
144
|
# keep only samples falling in a set of intervals
|
|
148
145
|
#
|
|
149
146
|
# arguments:
|
|
@@ -172,9 +169,9 @@ def restrict(samples,intervals,shift=False,s_ind=False,i_ind=False):
|
|
|
172
169
|
|
|
173
170
|
# assign and index to each sample time, only odd indeces belong to intervals
|
|
174
171
|
ind = np.digitize(samples[:,0],intervals.flatten())
|
|
175
|
-
|
|
176
|
-
samples = samples[
|
|
177
|
-
Ii = (ind[
|
|
172
|
+
Is = (ind % 2).astype(bool) # Is[i] is 1 iff samples[i] must be kept
|
|
173
|
+
samples = samples[Is]
|
|
174
|
+
Ii = (ind[Is] - 1) // 2 # Ii[j] indexes interval that contains samples[Is][j]
|
|
178
175
|
|
|
179
176
|
if shift:
|
|
180
177
|
# cumulative shifts: distances between intervals
|
|
@@ -188,16 +185,16 @@ def restrict(samples,intervals,shift=False,s_ind=False,i_ind=False):
|
|
|
188
185
|
samples = samples.reshape(-1)
|
|
189
186
|
|
|
190
187
|
if not s_ind and not i_ind:
|
|
191
|
-
return samples
|
|
192
|
-
out = (samples,
|
|
188
|
+
return samples
|
|
189
|
+
out = (samples, Is, Ii) # prepare tuple to return requested outputs
|
|
193
190
|
return out[:2+i_ind:2-s_ind]
|
|
194
191
|
|
|
195
192
|
|
|
196
|
-
def unshift(samples,intervals):
|
|
193
|
+
def unshift(samples, intervals):
|
|
197
194
|
# perform the opposite operation of option 'shift' in restrict
|
|
198
195
|
#
|
|
199
196
|
# arguments:
|
|
200
|
-
# samples (
|
|
197
|
+
# samples (:,:) float, rows are [time stamp, value1, value2, ...]; if 1d, reshaped to (:,1)
|
|
201
198
|
# intervals (:,2) float, rows are [start, stop] times (s) for an interval, previously used to shift `samples`
|
|
202
199
|
|
|
203
200
|
samples = np.asarray(samples,dtype=float)
|
|
@@ -219,56 +216,106 @@ def unshift(samples,intervals):
|
|
|
219
216
|
return samples
|
|
220
217
|
|
|
221
218
|
|
|
222
|
-
def shuffleEvents(events,offset=
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
219
|
+
def shuffleEvents(events, offset:float=None, n:int=None, group=None, fast:bool=None, intervals=None):
|
|
220
|
+
'''
|
|
221
|
+
shuffle point-process events (i.e., time stamps) preserving their inter-event interval
|
|
222
|
+
|
|
223
|
+
arguments:
|
|
224
|
+
events (s,) | (s,f) float, samples to shuffle; m: # samples, f: # features, if 2d, first column contains time stamps
|
|
225
|
+
offset float = 0 s, reference time, necessary when events start at a time != 0, e.g., for a portion of a recording
|
|
226
|
+
in [1000 s, 3000 s]; must be smaller than first event time, ignored if 'intervals' are provided
|
|
227
|
+
n int = 1, number of independent repetitions of the shuffling procedure, changes output shape
|
|
228
|
+
group (s,) int, grouping variable to shuffle events separately per group, adds output 'group'
|
|
229
|
+
fast bool = False, if True, 'events' must be time sorted (to save computation time)
|
|
230
|
+
intervals (:,2) float = None, rows are [start, stop] times of intervals to restrict shuffle into; if 1d, reshaped to (1,2)
|
|
231
|
+
|
|
232
|
+
output:
|
|
233
|
+
shuffled (:,:,) float, shuffled events, shape depends on 'n':
|
|
234
|
+
- same as 'events' if 'n' = 1
|
|
235
|
+
- (s,f,n), i.e., with an extra dimension for 'n' shuffling repetitions, otherwise
|
|
236
|
+
group (:,) int, optional, grouping variable for shuffled events, only returned if 'group' is not None, shape depends on 'n':
|
|
237
|
+
- (s,) if 'n' = 1
|
|
238
|
+
- (s,n), otherwise
|
|
239
|
+
'''
|
|
240
|
+
|
|
241
|
+
# default values
|
|
234
242
|
events = np.asarray(events)
|
|
243
|
+
n_dim = events.ndim
|
|
244
|
+
if offset is None: offset = 0
|
|
245
|
+
if n is None: n = 1
|
|
246
|
+
if group is not None: group = np.asarray(group,dtype=int)
|
|
235
247
|
if intervals is not None:
|
|
236
|
-
|
|
237
|
-
n_dim = events.ndim
|
|
238
|
-
events = restrict(events,intervals,shift=True)
|
|
248
|
+
events, valid = restrict(events,intervals,shift=True,s_ind=True)
|
|
239
249
|
offset = 0
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
250
|
+
if group is not None:
|
|
251
|
+
group = group[valid]
|
|
252
|
+
|
|
253
|
+
# zero-repetitions case
|
|
254
|
+
if n == 0:
|
|
255
|
+
if group is not None:
|
|
256
|
+
return np.zeros(events.shape+(0,)), np.zeros((events.shape[0],0))
|
|
257
|
+
return np.zeros(events.shape+(0,))
|
|
258
|
+
|
|
259
|
+
# make 'events' 3d by appending singleton dimensions
|
|
260
|
+
events = events.reshape(*events.shape, *(1,) * (3 - n_dim)) # (events, features, 1)
|
|
261
|
+
|
|
262
|
+
# sort by time
|
|
263
|
+
if not fast:
|
|
264
|
+
order = events[:,0,0].argsort(axis=0)
|
|
265
|
+
events = events[order]
|
|
266
|
+
if group is not None:
|
|
267
|
+
group = group[order]
|
|
268
|
+
|
|
269
|
+
# repeat events 'n' times
|
|
270
|
+
events = np.tile(events,(1,1,n)) # (events, features, shuffle repetitions)
|
|
271
|
+
|
|
272
|
+
# core f to shuffle events of a group
|
|
273
|
+
rng = np.random.default_rng()
|
|
274
|
+
def shuffleGroup(e):
|
|
275
|
+
# shuffle inter-event intervals (IEI)
|
|
276
|
+
iei = np.diff(e[:,0,:],prepend=offset,axis=0)
|
|
277
|
+
iei_shape = iei.shape
|
|
278
|
+
permutation_idx = np.argsort(rng.random(iei_shape),axis=0)
|
|
279
|
+
iei = iei[permutation_idx,np.arange(iei_shape[1])]
|
|
280
|
+
# reconstruct shuffled samples
|
|
281
|
+
shuffled = offset + np.cumsum(iei,axis=0) # shuffled times (events, shuffle repetitions)
|
|
282
|
+
features = e[permutation_idx[:,None,:], np.arange(1,e.shape[1])[None,:,None], np.arange(iei_shape[1])[None,None,:]]
|
|
283
|
+
shuffled = np.concatenate((shuffled[:,None],features),axis=1) # append shuffled features
|
|
284
|
+
return shuffled
|
|
285
|
+
|
|
286
|
+
# perform shuffling
|
|
287
|
+
if group is None:
|
|
288
|
+
shuffled = shuffleGroup(events)
|
|
251
289
|
else:
|
|
252
|
-
|
|
253
|
-
times = events[:,0].reshape((-1,1))
|
|
254
|
-
ids = events[:,1:]
|
|
255
|
-
unique_ids = np.unique(ids,axis=0)
|
|
256
|
-
|
|
257
290
|
shuffled = []
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
inter_event_intervals = np.random.permutation(inter_event_intervals)
|
|
262
|
-
shuffled.append(np.concatenate((offset+np.cumsum(inter_event_intervals,axis=0),np.repeat(i.reshape((1,-1)),len(inter_event_intervals),axis=0)),axis=1))
|
|
263
|
-
|
|
291
|
+
unique_groups, g_count = np.unique(group,return_counts=True)
|
|
292
|
+
for g in unique_groups:
|
|
293
|
+
shuffled.append(shuffleGroup(events[group==g]))
|
|
264
294
|
# sort by time
|
|
265
|
-
shuffled = np.concatenate(shuffled)
|
|
266
|
-
|
|
267
|
-
|
|
295
|
+
shuffled = np.concatenate(shuffled,axis=0)
|
|
296
|
+
order = shuffled[:,0,:].argsort(axis=0)
|
|
297
|
+
shuffled = shuffled[order[:,None,:], np.arange(shuffled.shape[1])[None,:,None], np.arange(shuffled.shape[2])[None,None,:]]
|
|
298
|
+
# build 'group' for output
|
|
299
|
+
group = np.repeat(unique_groups,g_count) # use unique output to sort
|
|
300
|
+
group = group[order] # sort as 'shuffled'
|
|
301
|
+
|
|
302
|
+
# unshift events back into intervals
|
|
268
303
|
if intervals is not None:
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
304
|
+
for i in range(n):
|
|
305
|
+
shuffled[:,:,i] = unshift(shuffled[:,:,i],intervals)
|
|
306
|
+
|
|
307
|
+
# restore shape
|
|
308
|
+
if n_dim == 1 and n == 1:
|
|
309
|
+
shuffled = shuffled.ravel()
|
|
310
|
+
elif n == 1:
|
|
311
|
+
shuffled = shuffled[:,:,0]
|
|
312
|
+
|
|
313
|
+
if group is not None:
|
|
314
|
+
if n_dim == 1 and n == 1:
|
|
315
|
+
group = group.ravel()
|
|
316
|
+
elif n == 1:
|
|
317
|
+
group = group[:,0]
|
|
318
|
+
return shuffled, group
|
|
272
319
|
|
|
273
320
|
return shuffled
|
|
274
321
|
|
|
@@ -315,5 +362,7 @@ def subtractIntervals(a,b):
|
|
|
315
362
|
c[new_even_ind] = a[even_ind]
|
|
316
363
|
# return as interval set
|
|
317
364
|
c = c.reshape((c.shape[0]//2,2))
|
|
365
|
+
# remove remaining empty intervals
|
|
366
|
+
c = c[(np.diff(c)!=0).ravel(),:]
|
|
318
367
|
|
|
319
368
|
return c
|
|
@@ -12,7 +12,7 @@ from collections.abc import Iterable
|
|
|
12
12
|
from typing import Literal, Callable
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
def adjustAxes(axs:Iterable[mpla.Axes], format:Literal['paper','poster']='paper'):
|
|
15
|
+
def adjustAxes(axs:mpla.Axes|Iterable[mpla.Axes], format:Literal['paper','poster']='paper'):
|
|
16
16
|
# adjust axes properties to emprove figure appearance
|
|
17
17
|
#
|
|
18
18
|
# arguments:
|
|
@@ -31,11 +31,11 @@ def adjustAxes(axs:Iterable[mpla.Axes], format:Literal['paper','poster']='paper'
|
|
|
31
31
|
|
|
32
32
|
for ax in axs:
|
|
33
33
|
|
|
34
|
-
# remove upper and right borders
|
|
35
|
-
ax.spines[['right','top']]
|
|
34
|
+
# remove upper and right borders in catesian axis
|
|
35
|
+
[ax.spines[spine].set_visible(False) for spine in ['right','top'] if spine in ax.spines]
|
|
36
36
|
|
|
37
37
|
# adjust thickness and font size
|
|
38
|
-
ax.spines[['bottom','left']]
|
|
38
|
+
[ax.spines[spine].set_linewidth(lw) for spine in ['bottom','left','polar'] if spine in ax.spines]
|
|
39
39
|
ax.tick_params(width=axw,labelsize=axtick)
|
|
40
40
|
ax.xaxis.label.set_fontsize(axfont)
|
|
41
41
|
ax.yaxis.label.set_fontsize(axfont)
|
|
@@ -43,7 +43,7 @@ def adjustAxes(axs:Iterable[mpla.Axes], format:Literal['paper','poster']='paper'
|
|
|
43
43
|
return
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
def makeFigure(title:str=None, n:tuple[int,int]=[1,1], size:tuple[float,float]=[20,10], projection=None, format:Literal['paper','poster']='paper'):
|
|
46
|
+
def makeFigure(title:str=None, n:tuple[int,int]=[1,1], size:tuple[float,float]=[20,10], projection:str=None, format:Literal['paper','poster']='paper'):
|
|
47
47
|
# make a figure
|
|
48
48
|
#
|
|
49
49
|
# arguments:
|
|
@@ -72,7 +72,7 @@ def makeFigure(title:str=None, n:tuple[int,int]=[1,1], size:tuple[float,float]=[
|
|
|
72
72
|
return fig, axs
|
|
73
73
|
|
|
74
74
|
|
|
75
|
-
def setProp(axs:Iterable[mpla.Axes], xlabelcolor:dict[int,mplt.ColorType]=None, xtickvisible:dict[int,bool]=None, **kwargs):
|
|
75
|
+
def setProp(axs:mpla.Axes|Iterable[mpla.Axes], xlabelcolor:dict[int,mplt.ColorType]=None, xtickvisible:dict[int,bool]=None, **kwargs):
|
|
76
76
|
# set multiple axes properties at once
|
|
77
77
|
#
|
|
78
78
|
# arguments:
|
|
@@ -104,7 +104,7 @@ def setProp(axs:Iterable[mpla.Axes], xlabelcolor:dict[int,mplt.ColorType]=None,
|
|
|
104
104
|
return
|
|
105
105
|
|
|
106
106
|
|
|
107
|
-
def saveFigure(fig,fname,format):
|
|
107
|
+
def saveFigure(fig, fname:str, format:str):
|
|
108
108
|
|
|
109
109
|
# promote single format to iterable
|
|
110
110
|
if isinstance(format,str):
|
|
@@ -115,7 +115,7 @@ def saveFigure(fig,fname,format):
|
|
|
115
115
|
return
|
|
116
116
|
|
|
117
117
|
|
|
118
|
-
def plotXY(data,start=None,stop=None,color=None,label=None,ax=None):
|
|
118
|
+
def plotXY(data, start=None, stop=None, color:mplt.ColorType=None, label=None, ax:mpla.Axes=None):
|
|
119
119
|
# plot columns of 'data', interpreting the first as the x axis and all others as y values
|
|
120
120
|
|
|
121
121
|
data = np.array(data, ndmin=2)
|
|
@@ -141,23 +141,26 @@ def plotXY(data,start=None,stop=None,color=None,label=None,ax=None):
|
|
|
141
141
|
return
|
|
142
142
|
|
|
143
143
|
|
|
144
|
-
def plotColorMap(data:
|
|
145
|
-
xzoom:
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
144
|
+
def plotColorMap(data:npt.NDArray[np.floating], vmin:float=None, vmax:float=None, zscore=None, sortby:npt.NDArray[np.floating]|Callable|str=None, sortax:int=0,
|
|
145
|
+
xzoom:float=None, yzoom:float=None, x=None, y=None, aspect:float=3/4, ax:mpla.Axes=None):
|
|
146
|
+
'''
|
|
147
|
+
plot a 2D array as a colormap with optional normalization, sorting, and resampling
|
|
148
|
+
|
|
149
|
+
arguments:
|
|
150
|
+
data (n,m) float, data to visualize, rows correspond to first dimension
|
|
151
|
+
vmin, vmax float = None, lower / upper bound of the colormap, if None uses autoscale
|
|
152
|
+
zscore int | 'all' = None, if int, specifies axis along which to z-score `data`; if 'all', compute z-score over whole array,
|
|
153
|
+
if None, no normalization is applied
|
|
154
|
+
sortby (:,) int | callable | 'peak' = None, optional method used to sort `data` along `sortax` (after optional z-scoring), either:
|
|
155
|
+
- 'peak', sort rows or columns by the index of their maximum value along the opposite axis
|
|
156
|
+
- (:,) int, array of indeces to sort data along `sortax`
|
|
157
|
+
- callable, must have signature ``f(data) -> array_like`` and return a 1D array used to sort data along `sortax`
|
|
158
|
+
sortax int = 0, axis along which sorting is performed
|
|
159
|
+
xzoom, yzoom float = None, optional horizontal / vertical resampling factor passed to ``scipy.ndimage.zoom`` (after sorting)
|
|
160
|
+
x, y (:,) float, coordinates corresponding to columns and rows of `data`, defaults are range(m) and range(n)
|
|
161
|
+
aspect float = 3/4, image aspect ratio
|
|
162
|
+
ax matplotlib.axes.Axes = matplotlib.pyplot.gca(), axes to plot in
|
|
163
|
+
'''
|
|
161
164
|
|
|
162
165
|
# store original shape in case data neds to be zoomed
|
|
163
166
|
n_y, n_x = data.shape
|
|
@@ -165,12 +168,15 @@ def plotColorMap(data: npt.NDArray[np.floating], vmin: float = None, vmax: float
|
|
|
165
168
|
if zscore is not None:
|
|
166
169
|
if zscore == 'all':
|
|
167
170
|
zscore = None
|
|
168
|
-
data = spst.zscore(data,axis=zscore)
|
|
171
|
+
data = spst.zscore(data,axis=zscore,nan_policy='omit')
|
|
169
172
|
|
|
170
173
|
if sortby is not None:
|
|
171
|
-
if sortby == 'peak':
|
|
174
|
+
if isinstance(sortby,str) and sortby == 'peak':
|
|
172
175
|
sortby = lambda x : np.argsort(np.argmax(x,1-sortax))
|
|
173
|
-
|
|
176
|
+
elif callable(sortby):
|
|
177
|
+
sort_idx = sortby(data)
|
|
178
|
+
else:
|
|
179
|
+
sort_idx = sortby
|
|
174
180
|
data = data[sort_idx,:] if sortax == 0 else data[:,sort_idx]
|
|
175
181
|
|
|
176
182
|
if xzoom is not None or yzoom is not None:
|
|
@@ -197,21 +203,26 @@ def plotColorMap(data: npt.NDArray[np.floating], vmin: float = None, vmax: float
|
|
|
197
203
|
return im
|
|
198
204
|
|
|
199
205
|
|
|
200
|
-
def semPlot(x, y, ci:Callable=None, zscore:bool|int=None, color=None, mode:Literal['area','error']='area', alpha:float=0.5, label:str=None, lprop:dict=None, aprop:dict=None, ax:mpla.Axes=None):
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
206
|
+
def semPlot(x, y, ci:str|Callable=None, zscore:bool|int=None, color:mplt.ColorType=None, mode:Literal['area','error']='area', alpha:float=0.5, label:str=None, lprop:dict=None, aprop:dict=None, ax:mpla.Axes=None):
|
|
207
|
+
'''
|
|
208
|
+
plot mean +/- confidence intervals of matrix data
|
|
209
|
+
|
|
210
|
+
arguments:
|
|
211
|
+
x (n) float, x coordinates
|
|
212
|
+
y (:,n) float, data to plot, each column corresponds to a value of x
|
|
213
|
+
ci callable | 'nansem', used to compute confidence intervals for every column of `y`, either:
|
|
214
|
+
- 'nansem', standard error of the mean (SEM) for each column of `y`, ignoring missing values
|
|
215
|
+
- callable, must have signature ``low, high = ci(y)``
|
|
216
|
+
zscore bool = False | int, if True (or 1), z-score w.r.t. average of y, if 2, z-score each row of y independently
|
|
217
|
+
color color = None
|
|
218
|
+
mode str = 'area' | 'error', plot 'ci' either as a shaded area or as error bars
|
|
219
|
+
alpha float = 0.5, shaded area transparency value (only for 'area' mode)
|
|
220
|
+
label str = None, legend label for line
|
|
221
|
+
lprop dict = {}, keyword arguments passed to matplotlib.pyplot.plot
|
|
222
|
+
aprop dict = {}, keyword arguments passed to matplotlib.pyplot.fill_between (only for 'area' mode)
|
|
223
|
+
ax matplotlib.axes.Axes = matplotlib.pyplot.gca(), axes to plot in
|
|
224
|
+
'''
|
|
225
|
+
# NOTE: should maybe change y to match plt.plot: each row of 'y' corresponds to a values of 'x'
|
|
215
226
|
|
|
216
227
|
y = np.array(y,ndmin=2)
|
|
217
228
|
y = y[~np.isnan(y).all(axis=1)] # ŕemove full-nan rows
|
|
@@ -227,7 +238,11 @@ def semPlot(x, y, ci:Callable=None, zscore:bool|int=None, color=None, mode:Liter
|
|
|
227
238
|
if not (set(['edgecolor','edgecolors','ec','facecolor','facecolors','fc','color']) & aprop.keys()): aprop['color'] = color
|
|
228
239
|
aprop.setdefault('alpha',alpha)
|
|
229
240
|
aprop.setdefault('lw',0)
|
|
230
|
-
|
|
241
|
+
|
|
242
|
+
if isinstance(ci,str) and ci == 'nansem':
|
|
243
|
+
ci = lambda x : (np.nanmean(x,axis=0) - np.nanstd(x,axis=0,ddof=1)/np.sqrt(np.sum(~np.isnan(x),axis=0)),
|
|
244
|
+
np.nanmean(x,axis=0) + np.nanstd(x,axis=0,ddof=1)/np.sqrt(np.sum(~np.isnan(x),axis=0)))
|
|
245
|
+
elif ci is None:
|
|
231
246
|
if y.shape[0] == 1:
|
|
232
247
|
ci = lambda x : (x.flatten(), x.flatten())
|
|
233
248
|
elif y.shape[0] < 500:
|
|
@@ -268,7 +283,7 @@ def semPlot(x, y, ci:Callable=None, zscore:bool|int=None, color=None, mode:Liter
|
|
|
268
283
|
return
|
|
269
284
|
|
|
270
285
|
|
|
271
|
-
def boxPlot(data, x=None, color=None, label=None, ax:mpla.Axes=None):
|
|
286
|
+
def boxPlot(data, x=None, color:mplt.ColorType=None, label=None, ax:mpla.Axes=None):
|
|
272
287
|
|
|
273
288
|
if ax is None:
|
|
274
289
|
ax = plt.gca()
|
|
@@ -282,6 +297,7 @@ def boxPlot(data, x=None, color=None, label=None, ax:mpla.Axes=None):
|
|
|
282
297
|
else:
|
|
283
298
|
data = [np.array(d)[~np.isnan(d)] for d in data]
|
|
284
299
|
|
|
300
|
+
if color is None: color = 'b'
|
|
285
301
|
try:
|
|
286
302
|
color = mplc.to_rgba(color)
|
|
287
303
|
color = [color] * len(data)
|
|
@@ -372,7 +388,7 @@ def pBar(p, x = None, alpha=0.05, dy=1, draw=(False,True,True,True), ax:mpla.Axe
|
|
|
372
388
|
return
|
|
373
389
|
|
|
374
390
|
|
|
375
|
-
def pHorzLine(p,t=None,dy=None,color=None,ax=None):
|
|
391
|
+
def pHorzLine(p, t=None, dy=None, color:mplt.ColorType=None, ax:mpla.Axes=None):
|
|
376
392
|
# p: (n_times, n_cond)
|
|
377
393
|
|
|
378
394
|
p = np.asarray(p).astype(float)
|
|
@@ -403,7 +419,7 @@ def pHorzLine(p,t=None,dy=None,color=None,ax=None):
|
|
|
403
419
|
return
|
|
404
420
|
|
|
405
421
|
|
|
406
|
-
def plotIntervals(intervals,alpha=0.3,color='gray',ax:mpla.Axes=None):
|
|
422
|
+
def plotIntervals(intervals, alpha=0.3, color:mplt.ColorType='gray', ax:mpla.Axes=None):
|
|
407
423
|
|
|
408
424
|
intervals = np.array(intervals,ndmin=2)
|
|
409
425
|
if intervals.ndim != 2 or intervals.shape[1] != 2:
|
|
@@ -416,12 +432,29 @@ def plotIntervals(intervals,alpha=0.3,color='gray',ax:mpla.Axes=None):
|
|
|
416
432
|
ax.axvspan(start,stop,alpha=alpha,color=color)
|
|
417
433
|
|
|
418
434
|
|
|
419
|
-
def plotPDF(x,
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
435
|
+
def plotPDF(x, mode:str=None, bandwidth:float|str=None, eps:float=None, n_points:int=None, norm=None, color:mplt.ColorType=None, label=None, ax:mpla.Axes=None, **plot_kwargs):
|
|
436
|
+
'''
|
|
437
|
+
estimate and plot probability density function (PDF) of data
|
|
438
|
+
|
|
439
|
+
arguments:
|
|
440
|
+
x (n,) tuple | array, data to plot
|
|
441
|
+
mode str = {'normal','log','polar'}, DESCRIBE
|
|
442
|
+
log bool = False, plot log-transformed PDF, DEPRECATED
|
|
443
|
+
bandwidth float | str = 'scott', bandwidth for gaussian kernel
|
|
444
|
+
eps float = 1e-12, small value used to avoid log(0)
|
|
445
|
+
n_points int = 50, number of points used to evaluate PDF
|
|
446
|
+
norm str = {'density','max'}, normalization mode, 'density' computes PDF, 'max' normalizes its maximum to 1
|
|
447
|
+
color color = None, line color
|
|
448
|
+
label str = None, legend label for line
|
|
449
|
+
ax matplotlib.axes.Axes = matplotlib.pyplot.gca(), axes to plot in
|
|
450
|
+
'''
|
|
451
|
+
|
|
452
|
+
if mode is None: mode = 'normal'
|
|
453
|
+
if bandwidth is None: bandwidth = 0.05 if mode == 'polar' else 'scott'
|
|
454
|
+
if eps is None: eps = 1e-12
|
|
455
|
+
if n_points is None: n_points = 50
|
|
456
|
+
if norm is None: norm = 'density'
|
|
457
|
+
if ax is None: ax = plt.gca()
|
|
425
458
|
if isinstance(x,tuple):
|
|
426
459
|
if color is None:
|
|
427
460
|
color = [None] * len(x)
|
|
@@ -437,30 +470,54 @@ def plotPDF(x, log:bool=False, bandwidth:float|str=None, eps:float=1e-12, n_poin
|
|
|
437
470
|
for i, data in enumerate(x):
|
|
438
471
|
# cast to array and validate
|
|
439
472
|
data = np.asarray(data)
|
|
473
|
+
data = data[~np.isnan(data)] # always ravels input: loosing a capability of gaussian_kde?
|
|
474
|
+
#if data.ndim == 2 and data.shape[1] == 1:
|
|
475
|
+
# data = data.ravel()
|
|
440
476
|
if data.size < 2:
|
|
441
477
|
grid.append([])
|
|
442
478
|
density.append([])
|
|
443
479
|
continue
|
|
444
|
-
data = data[~np.isnan(data)] # always ravels input: loosing a capability of gaussian_kde?
|
|
445
|
-
#if data.ndim == 2 and data.shape[1] == 1:
|
|
446
|
-
# data = data.ravel()
|
|
447
|
-
|
|
448
|
-
if log:
|
|
449
|
-
if data.min() <= 0:
|
|
450
|
-
raise ValueError('log-transforming data requires positive values')
|
|
451
|
-
data = np.log(data + eps) # log-transform data
|
|
452
|
-
this_grid = np.linspace(data.min(),data.max(),n_points) # linear grid in log-space
|
|
453
|
-
jacobian = np.exp(this_grid) # jacobian term to transform density back to linear
|
|
454
|
-
else:
|
|
455
|
-
this_grid = np.linspace(data.min(),data.max(),n_points) # linear grid
|
|
456
480
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
481
|
+
match mode:
|
|
482
|
+
# 1. real-valued data using gaussian kernel density estimator
|
|
483
|
+
case 'normal':
|
|
484
|
+
this_grid = np.linspace(data.min(),data.max(),n_points) # linear grid
|
|
485
|
+
kde = sp.stats.gaussian_kde(data,bw_method=bandwidth)
|
|
486
|
+
this_density = kde(this_grid)
|
|
487
|
+
if norm == 'max':
|
|
488
|
+
this_density /= this_density.max()
|
|
489
|
+
ax.plot(this_grid,this_density,color=color[i],label=label[i],**plot_kwargs)
|
|
490
|
+
|
|
491
|
+
# 2. log-transformed data using gaussian kernel density estimator
|
|
492
|
+
case 'log':
|
|
493
|
+
if data.min() <= 0:
|
|
494
|
+
raise ValueError('log-transforming data requires positive values')
|
|
495
|
+
data = np.log(data + eps) # log-transform data
|
|
496
|
+
this_grid = np.linspace(data.min(),data.max(),n_points) # linear grid in log-space
|
|
497
|
+
jacobian = np.exp(this_grid) # jacobian term to transform density back to linear
|
|
498
|
+
kde = sp.stats.gaussian_kde(data,bw_method=bandwidth)
|
|
499
|
+
this_density = kde(this_grid) / jacobian
|
|
500
|
+
if norm == 'max':
|
|
501
|
+
this_density /= this_density.max()
|
|
502
|
+
ax.loglog(jacobian,this_density,color=color[i],label=label[i],**plot_kwargs)
|
|
503
|
+
|
|
504
|
+
# 3. circular data using von-Mises kernel density estimator
|
|
505
|
+
case 'polar':
|
|
506
|
+
data = np.mod(data,2*np.pi)
|
|
507
|
+
this_grid = np.linspace(0,2*np.pi,n_points) # linear grid in [0,2*pi]
|
|
508
|
+
this_density = np.zeros_like(this_grid)
|
|
509
|
+
for theta in data:
|
|
510
|
+
this_density += spst.vonmises.pdf(this_grid-theta,1/bandwidth)
|
|
511
|
+
if norm == 'max':
|
|
512
|
+
this_density /= this_density.max()
|
|
513
|
+
else:
|
|
514
|
+
this_density /= len(data)
|
|
515
|
+
this_grid = np.concatenate((this_grid,this_grid+2*np.pi))
|
|
516
|
+
this_density = np.concatenate((this_density,this_density))
|
|
517
|
+
ax.plot(this_grid,this_density,color=color[i],label=label[i],**plot_kwargs)
|
|
518
|
+
|
|
519
|
+
grid.append(this_grid)
|
|
520
|
+
density.append(this_density)
|
|
464
521
|
|
|
465
522
|
ax.set_yticks([])
|
|
466
523
|
|
|
@@ -57,11 +57,14 @@ class regions:
|
|
|
57
57
|
# 2. assign session epochs
|
|
58
58
|
matches = self._matchEvents(phase_names,phases) if phases else phase_names
|
|
59
59
|
self.phases = {m: np.stack((loaded_events[m]['beginning'],loaded_events[m]['end']),axis=1) for m in matches}
|
|
60
|
+
epoch_intervals = np.concatenate(list(self.phases.values()))
|
|
60
61
|
|
|
61
62
|
# 3. assign states
|
|
62
63
|
self.states = {s: np.stack((loaded_events[s]['col0'],loaded_events[s]['col1']),axis=1) for s in states}
|
|
63
|
-
# if session phases are available, use them to compute special states 'all' and 'other' SHOULD ALSO RESTRICT states AND events TO phases
|
|
64
64
|
if phase_names:
|
|
65
|
+
# restrict states to session epochs
|
|
66
|
+
self.states = {s: fmatoolbox.general.restrict(self.states[s],epoch_intervals) for s in self.states}
|
|
67
|
+
# build special states 'all' and 'other'
|
|
65
68
|
self.states['all'] = np.array([[self.phases[list(self.phases)[0]][0,0],self.phases[list(self.phases)[-1]][-1,-1]]])
|
|
66
69
|
self.states['other'] = self.states['all']
|
|
67
70
|
for name in states:
|
|
@@ -90,6 +93,12 @@ class regions:
|
|
|
90
93
|
e['intervals'] = np.stack((e[start],e[end]),axis=1)
|
|
91
94
|
e.pop(start)
|
|
92
95
|
e.pop(end)
|
|
96
|
+
|
|
97
|
+
# restrict to session epochs
|
|
98
|
+
if phase_names:
|
|
99
|
+
_, valid = fmatoolbox.general.restrict(e['intervals'],epoch_intervals,s_ind=True)
|
|
100
|
+
e = {k: v[valid] for k, v in e.items()}
|
|
101
|
+
|
|
93
102
|
self.events[name] = e
|
|
94
103
|
|
|
95
104
|
if ids:
|
|
@@ -115,7 +124,7 @@ class regions:
|
|
|
115
124
|
if not self.all_events:
|
|
116
125
|
# restrict spikes to session phases
|
|
117
126
|
for id in self.ids:
|
|
118
|
-
self.region[id]['spikes'] = fmatoolbox.general.restrict(self.region[id]['spikes'],
|
|
127
|
+
self.region[id]['spikes'] = fmatoolbox.general.restrict(self.region[id]['spikes'],epoch_intervals)
|
|
119
128
|
|
|
120
129
|
return
|
|
121
130
|
|
|
@@ -369,23 +378,25 @@ class regions:
|
|
|
369
378
|
|
|
370
379
|
def firingRate(self, regs:Iterable[str]=None, e_groups:Iterable[int]=None, states:Iterable[str]=None, when=None, shift=False,
|
|
371
380
|
window=None, step=None, smooth=None, norm=False):
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
381
|
+
'''
|
|
382
|
+
get region firing rate
|
|
383
|
+
|
|
384
|
+
arguments:
|
|
385
|
+
regs (n) string = None, brain regions to compute firing rate of, default is all regions
|
|
386
|
+
e_groups (m) int = None, electrode groups (starting at 1) to compute firing rate of, default is none
|
|
387
|
+
states (:) string = None, behavioral states, default is all
|
|
388
|
+
when DESCRIBE, same input as eventIntervals
|
|
389
|
+
shift bool = False, shift epochs together in time after filtering by state
|
|
390
|
+
window float = 0.05, window size to count spikes
|
|
391
|
+
step int = 1, firing rate is computed in windows of length 'binSize' and overlap 'binSize' / 'step',
|
|
392
|
+
default is no overlap
|
|
393
|
+
smooth float = None, gaussian kernel std for smoothing over time
|
|
394
|
+
norm bool = False, normalize by neuron number per region
|
|
395
|
+
|
|
396
|
+
output:
|
|
397
|
+
rate (:,n+m+1) float, every row is [time stamp, firing rates for n regions, firing rates for m electrodes],
|
|
398
|
+
input order of regions and electrodes is preserved
|
|
399
|
+
'''
|
|
389
400
|
|
|
390
401
|
if states is not None and when is not None:
|
|
391
402
|
raise ValueError("'states' and 'when' cannot be specified at the same time")
|
|
@@ -394,7 +405,8 @@ class regions:
|
|
|
394
405
|
|
|
395
406
|
regs, e_groups, states = self._checkIDs(regs=regs,e_groups=e_groups,states=states,fuse=True)
|
|
396
407
|
|
|
397
|
-
# find big holes in 'when' intervals, to speed up computation
|
|
408
|
+
# find big holes in 'when' intervals, to speed up computation
|
|
409
|
+
# NOTE: it is actually number of time bins which defines big! Also, this looks every element of spikes many times, maybe useless!
|
|
398
410
|
when = self.eventIntervals(when)
|
|
399
411
|
holes = when[1:,0] - when[:-1,1] > 5000 # s
|
|
400
412
|
partition_idx = np.insert(np.cumsum(holes),0,0) # partition_idx[i] indexes partition to which when[i] belongs
|
|
@@ -410,12 +422,12 @@ class regions:
|
|
|
410
422
|
|
|
411
423
|
if np.any(regs != None):
|
|
412
424
|
for r in regs:
|
|
413
|
-
fr = fmatoolbox.analysis.
|
|
425
|
+
fr = fmatoolbox.analysis.istantaneousRate(self.spikes(regs=r)[:,0],start=np.floor(intervals[0,0]),stop=intervals[-1,1],bin=window,step=step,smooth=smooth)
|
|
414
426
|
fr_interv.append(fr[:,1])
|
|
415
427
|
|
|
416
428
|
if np.any(e_groups != None):
|
|
417
429
|
for e in e_groups:
|
|
418
|
-
fr = fmatoolbox.analysis.
|
|
430
|
+
fr = fmatoolbox.analysis.istantaneousRate(self.spikes(e_groups=e)[:,0],start=np.floor(intervals[0,0]),stop=intervals[-1,1],bin=window,step=step,smooth=smooth)
|
|
419
431
|
fr_interv.append(fr[:,1])
|
|
420
432
|
|
|
421
433
|
firing_rate.append(np.stack(fr_interv,1))
|
|
@@ -428,7 +440,7 @@ class regions:
|
|
|
428
440
|
# filter by state
|
|
429
441
|
if np.any(states != 'all'):
|
|
430
442
|
firing_rate = fmatoolbox.general.restrict(firing_rate,self.eventIntervals([states]),shift=shift)
|
|
431
|
-
warnings.warn("option 'states' is deprecated, use 'when' instead")
|
|
443
|
+
warnings.warn("option 'states' is deprecated, use 'when' instead!")
|
|
432
444
|
|
|
433
445
|
# normalize
|
|
434
446
|
if norm:
|
|
@@ -468,7 +480,7 @@ class regions:
|
|
|
468
480
|
firing_rate = np.zeros((n_times[-1],n_units[-1]))
|
|
469
481
|
for i, interval in enumerate(phase_intervals):
|
|
470
482
|
for j, r in enumerate(regs):
|
|
471
|
-
fr = fmatoolbox.analysis.
|
|
483
|
+
fr = fmatoolbox.analysis.istantaneousRate(self.spikes(r),start=interval[0],stop=interval[1],bin=window,step=step,smooth=smooth,g_range=self.units(r))
|
|
472
484
|
firing_rate[n_times[i]:n_times[i+1],n_units[j]:n_units[j+1]] = fr[:,1:]
|
|
473
485
|
firing_rate[n_times[i]:n_times[i+1],0] = fr[:,0]
|
|
474
486
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fmatoolbox
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Python port of FMAToolbox
|
|
5
5
|
Author: Pietro Bozzo
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
7
7
|
Project-URL: Homepage, https://github.com/pietro-bozzo/FMAToolboxPython
|
|
8
8
|
Project-URL: Repository, https://github.com/pietro-bozzo/FMAToolboxPython
|
|
9
9
|
Project-URL: Issues, https://github.com/pietro-bozzo/FMAToolboxPython/issues
|
|
10
|
-
Requires-Python: >=3.
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE
|
|
13
13
|
Requires-Dist: numpy
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|