-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathTIR.py
More file actions
1907 lines (1634 loc) · 76.3 KB
/
TIR.py
File metadata and controls
1907 lines (1634 loc) · 76.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#0. software packages
#os
import os
import os.path
import sys
from time import time
from datetime import datetime
#basics
import numpy as np
from numpy.random import random
import pandas as pd
import math
import scipy
import pickle
from copy import deepcopy
#plotting
import matplotlib.pyplot as plt
from matplotlib.ticker import StrMethodFormatter
from matplotlib import rcParams
import matplotlib as mpl
from matplotlib import cm
from matplotlib import colors
from mpl_toolkits.axes_grid1 import make_axes_locatable
from matplotlib.offsetbox import (TextArea, DrawingArea, OffsetImage,AnnotationBbox)
#training
from sklearn.preprocessing import MinMaxScaler
from sklearn.model_selection import train_test_split
from sklearn import manifold
from scipy.linalg import orthogonal_procrustes,eigh
from scipy.stats import linregress
from sklearn.metrics.pairwise import euclidean_distances
from sklearn.metrics import mean_squared_error
# torch is imported lazily inside lift_returns_ann and the legacy ANN
# trainers so that TIR.py is usable for the non-ANN pipeline (delay vectors,
# diffusion maps, bootstrap CIs, Kabsch, panel-B helpers) on environments
# without torch installed.
try:
from torch import nn
from torch.utils.data import Dataset, TensorDataset, DataLoader
import torch.nn.parallel
from torch.optim import Adam
from torch.utils.data.dataset import random_split
_HAS_TORCH = True
except ImportError:
_HAS_TORCH = False
#Entropies
####################################################################################################################
# 1.1: Shanon Entropies
def shan_ts(ser,b):
"""
Compute the Shannon entropy of a time series.
Args:
ser: The time series (array).
b: The number of bins to use for the histogram (int).
Returns:
float: The Shannon entropy of the time series.
"""
c = np.histogram(ser,b)[0]
c_normalized = c / float(np.sum(c))
c_normalized = c_normalized[np.nonzero(c_normalized)]
H = -sum(c_normalized* np.log2(c_normalized))
return H
def shan(c):
"""
Compute the Shannon entropy from histogram counts.
Args:
c: The time series histogram (array).
Returns:
float: The Shannon entropy of the time series.
"""
c_normalized = c / float(np.sum(c))
c_normalized = c_normalized[np.nonzero(c_normalized)]
H = -sum(c_normalized* np.log2(c_normalized))
return H
####################################################################################################################
# 1.2: Autocorrelation and Mutual Information
def AC(ser,ts,dim=5,maxval=1000,tol=10,cut=0.2,save_loc='N',save=False,plots=False, method='cutoff'):
"""
Compute the autocorrelation of a time series.
Args:
ser: The time series (array).
ts: The time series name (string)
dim: The dimensionality of the time series (int).
maxval: The maximum value of tau to test (int).
tol: The time delay step size (int).
cut: The cutoff value determining at which time delay to stop, therby selecting the appropriate tau (float).
save_loc: The location to save the image (string).
save: A boolean to save the time series (boolean).
plots: A boolean to plot the plots (boolean).
method: The method to determine tau ('cutoff' or 'minimum') (string).
Returns:
int: The optimal time delay of the time series.
"""
# make an array of tested time delays from 0 to maxval with step size tol
tau_arr = np.arange(0,maxval,tol)
# define the mean, std and deviation from mean for each value in TS
mu, sigma= np.mean(ser), np.std(ser)
mz = ser-mu
#Store autocorrelations in array
AC_arr = []
#compute AC at each value of tau, t
for t in tau_arr:
val = np.mean(np.multiply(mz[0:len(mz)-t],mz[t:len(mz)]))/(sigma**2)
AC_arr.append(val)
#Determine cutoff by computing
if method == 'cutoff':
for tau_i in range(len(AC_arr)):
if AC_arr[tau_i] <= cut:
AC_length=tau_arr[tau_i]
break
#choose last val if no other option
elif tau_i == len(AC_arr)-1:
AC_length = int(tau_arr[tau_i]*tol)
elif method == 'minimum':
# Determine tau by finding the minimum autocorrelation value
#step 1: polyfit to get rid of noise
x = np.arange(len(AC_arr))
poly_coeffs = np.polyfit(x, AC_arr, 6)
poly_fit = np.polyval(poly_coeffs, x)
#step 2: find minimum
for jkl in range(1, len(poly_fit) - 1):
if poly_fit[jkl-1]>poly_fit[jkl] and poly_fit[jkl+1]>poly_fit[jkl]:
AC_length=int(jkl*tol)
break
else:
AC_length=int(np.argmin(AC_arr)*tol)
print(AC_length)
#plot AC
if plots==True:
fig, ax = plt.subplots()
ax.plot(tau_arr, AC_arr, 'b-', marker='o')
ax.plot([tau_arr[0],tau_arr[-1]], [0,0], 'k:')
ax.set_xlabel("delay time, tau (steps)")
ax.set_ylabel("autocorrelation, (arb. units)")
ax.figure.savefig("%s/AC_%s.pdf" %(save_loc,ts))
plt.draw()
#save param
print('Suggested time delay', AC_length)
if save==True:
np.savez('%s/AC_%s_tau%d_dim%d.npz' %(save_loc,ts,AC_length,dim),AC=AC_length)
plt.show()
plt.close()
return AC_length
####################################################################################################################
# 1.3: Mutual Information
def MI(x,y,b=100):
"""
Compute the autocorrelation of a time series.
Args:
x: The first time series (array).
y: The second time series (array).
b: The number of bins to use for the histogram (int).
Returns:
float: The mutual information between two time series
"""
c_x=np.histogram(x,bins=b)[0]
c_y=np.histogram(y,bins=b)[0]
c_xy=np.histogram2d(x,y,bins=b)[0]
return shan(c_x)+shan(c_y) - shan(c_xy)
####################################################################################################################
# 1.4: Draw Mutual Information Curve
def MI_curve(ser1,ser2,ts,b=100,maxval=1000,tol=1,save=False,save_loc='N',cut=0.2):
"""
Compute the MI curves for two time series
Args:
ser1: The first time series (array).
ser2: The second time series (array).
ts: The time series name (string).
b: The number of bins to use for the histogram (int).
maxval: The maximum value of tau to test (int).
tol: The time delay step size (int).
save: A boolean to save the time series (boolean).
save_loc: The location to save the image (string).
cut: The cutoff value determining at which time delay to stop, therby selecting the appropriate tau (float).
Returns:
float: The mutual information between two time series
int: The optimal time delay of the time series.
"""
MI_v = []
for iii in range(0,maxval,tol):
x = ser1[0:len(ser1)-iii]
y = ser2[iii:len(ser1)]
c_x=np.histogram(x,bins=b)[0]
c_y=np.histogram(y,bins=b)[0]
c_xy=np.histogram2d(x,y,bins=b)[0]
MI_v.append(shan(c_x)+shan(c_y) - shan(c_xy))
#step 1: polyfit to get rid of noise
x = np.arange(len(MI_v))
poly_coeffs = np.polyfit(x, MI_v, 6)
poly_fit = np.polyval(poly_coeffs, x)
#step 2: find minimum
for jkl in range(1, len(poly_fit) - 1):
if poly_fit[jkl-1]>poly_fit[jkl] and poly_fit[jkl+1]>poly_fit[jkl]:
MI_length=int(jkl*tol)
break
else:
MI_length=int(np.argmin(MI_v)*tol)
plt.scatter(range(len(MI_v)),MI_v,linewidth=3)
plt.xlabel('Time Delay (time steps)',fontsize=15)
plt.ylabel('Mutual Information (arb. units)',fontsize=15)
plt.show()
if save==True:
plt.savefig('%s/MI_%s.pdf' %(save_loc,ts))
plt.close()
return MI_v,MI_length
####################################################################################################################
# 1.5: Optimal embedding dimensionality (E1d)
def E1d_cal(ser,ts,stop,stride=10,tau=1,dim=5,dMax=25,start=0,tol=0.98,save_loc='N',save=False,plots=False):
"""
Estimate the optimal embedding dimensionality of a time series using the E1d method of Cao (1997).
Args:
ser: The time series (array).
ts: The time series name (string)
stop: The stopping point for the E1d analysis (int).
stride: The stride for the E1d analysis (int).
tau: The time delay for the E1d analysis (int).
dim: The maximum dimensionality for the E1d analysis (int).
dMax: The maximum dimensionality for the E1d analysis (int).
start: The starting point for the E1d analysis (int).
tol: The tolerance for the E1d analysis (float).
save_loc: The location to save the image (string).
save: A boolean to save the time series (boolean).
plots: A boolean to plot the plots (boolean).
Returns:
int: The optimal embedding dimensionality of the time series.
"""
# plotting delay vectors
if plots==True:
#construct delay vectors via Takens Theorem
delayVecs = np.reshape(ser[0:len(ser)-(dim-1)*tau],(len(ser)-(dim-1)*tau,1))
for ii in range(1,dim):
delayVecs = np.concatenate((delayVecs,np.reshape(ser[ii*tau:len(ser)-(dim-1)*tau+ii*tau],(len(ser)-(dim-1)*tau,1))), axis=1)
print('nVecs = %d' % delayVecs.shape[0])
plot_stride = 10
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
i,j,k=0,2,4
ax.scatter(delayVecs[::plot_stride,i], delayVecs[::plot_stride,j], delayVecs[::plot_stride,k])
ax.set_xlabel('delayVec_' + str(i))
ax.set_ylabel('delayVec_' + str(j))
ax.set_zlabel('delayVec_' + str(k))
plt.draw()
plt.show()
plt.close()
# Performing E1d analysis using specified value of tau
#initialize Ed and define max dim
Ed=[]
#build Takens delay vectors for each dimension
for d in range(1,dMax):
# building d-dim delay embedding of len(h2t)-(d-1)*tau delay points with delay time of tau (in steps)
delayVecs = np.reshape(ser[0:len(ser)-(d-1)*tau],(len(ser)-(d-1)*tau,1))
for ii in range(1,d):
delayVecs = np.concatenate((delayVecs,np.reshape(ser[ii*tau:len(ser)-(d-1)*tau+ii*tau],(len(ser)-(d-1)*tau,1))), axis=1)
# subsampling - check if start and stop are within bounds
if start<0 | stop>delayVecs.shape[0]:
print(stop,delayVecs.shape[0])
print('ERROR: subsampling illegal points')
break
delayVecs_ss = delayVecs[start:stop:stride,:]
# construct pairwise distance matrix between delay vectors pairwise distances between delay vectors
P = euclidean_distances(delayVecs_ss,delayVecs_ss)
idx = np.arange(0,delayVecs_ss.shape[0],1)
# computing a_id and a_i(d+1) for each point
if d >= 2:
Ed.append(np.mean(np.divide(P[idx,idxNN_d],deltaNN_d)))
# identifying non-zero nearest neighbor of each point
P[np.where(P==0)]=np.inf
idxNN_d = np.argmin(P, axis=1)
deltaNN_d = P[idx,idxNN_d]
# computing E1d
Ed = np.array(Ed)
E1d = np.divide( Ed[1:len(Ed)], Ed[0:len(Ed)-1] )
#select E1d that is odd and explains more than tol of variance that is also odd (oddness only matters with temporal symmetry)
for k in range(len(E1d)):
if k//2!=0 and E1d[k] >=tol:
E1dmax=k
break
#set to last value if no other option; report
else:
E1dmax=k
print('Increase dMax or reduce tolerance, no suitable E1d found')
if plots==True:
#make a plot
fig, ax = plt.subplots()
ax.plot(np.arange(2,len(E1d)+1), E1d[1::], 'k-', marker='o')
ax.set_xlabel("delay dim")
ax.set_ylabel("E1d")
ax.figure.savefig("%s/E1d_%s_tau%d_dim%d.pdf" %(save_loc,ts,tau,dim))
plt.draw()
if save==True:
np.savez('%s/E1d_%s_tau%d_dim%d.npz' %(save_loc,ts,tau,dim),E1d=E1dmax)
print('Suggested dimensionality: ', E1dmax )
return E1dmax
####################################################################################################################
#Scaling Functions
#2.1 Standard Scaling for Time Series
def scalearr(ser,ts='N',dim=5,tau=1,save_loc='N',save=False):
"""
Scale a time series to the range [0,1].
Args:
ser: The time series (array).
ts: The time series name (string)
dim: The dimensionality of the time series (int).
tau: The time delay of the time series (int).
save_loc: The location to save the scaling (string).
save: A boolean to save the scaling (boolean).
Returns:
array: The scaled time series (array).
"""
ser = ser.reshape(-1, 1)
scaler = MinMaxScaler(feature_range=(0,1))
scaled_ts = scaler.fit_transform(ser)
# saving scaling associated with model
if save==True:
D = {}
D['zscale'] = scaler
file = open('%s/scaled_%s.pkl' %(save_loc,ts), 'wb')
pickle.dump(D, file)
file.close()
np.savez('%s/scaled_%s_tau%d_dim%d.npz' %(save_loc,ts,tau,dim),scaled_ts=scaled_ts)
return scaled_ts
####################################################################################################################
# 2.2: Standard Scaling for Manifold
def unscalearr(Z_pred,load_loc,ts='N',dim=5,tau=1,save_loc='N',save=False):
"""
Unscale z-scaled manifold
Args:
Z_pred: The z-scaled manifold (array).
load_loc: The location to load the scaling (string).
ts: The time series name (string).
dim: The dimensionality of the time series (int).
tau: The time delay of the time series (int).
save_loc: The location to save the scaling (string).
save: A boolean to save the scaling (boolean).
Returns:
array: The unscaled manifold.
"""
D = pickle.load(open(load_loc, 'rb'))
zscale_aa = D['zscale']
z_pred = zscale_aa.inverse_transform(Z_pred)
if save==True:
np.savez('%s/unscaled_%s_tau%d_dim%d.npz' %(save_loc,ts,tau,dim),z_pred=z_pred)
return z_pred
####################################################################################################################
# 2.3: Standard Scaling for Pandas Dataframe
def scalepd(TS,ts,dim=5,tau=1,save_loc='N',save=False):
"""
Scale a time series to the range [0,1] from a pandas dataframe.
Args:
TS: The time series (dataframe).
ts: The time series name (string).
dim: The dimensionality of the time series as computed by E1d (int).
tau: The time delay of the time series (int).
save_loc: The location to save the scaling (string).
save: A boolean to save the scaling (boolean).
Returns:
array: The scaled time series.
"""
ser = np.array(TS.loc[:,ts]).reshape(-1, 1)
scaler = MinMaxScaler(feature_range=(0,1))
scaled_ts = scaler.fit_transform(ser)
# saving scaling associated with model
if save==True:
D = {}
D['zscale'] = scaler
file = open('%s/scaled_%s.pkl' %(save_loc,ts), 'wb')
pickle.dump(D, file)
file.close()
np.savez('%s/scaled_%s_tau%d_dim%d.npz' %(save_loc,ts,tau,dim),scaled_ts=scaled_ts)
return scaled_ts
# Delay Vector construction
####################################################################################################################
# 3. Structure data (singlevairate, multivariate, multitemporal)
def make_dvecs(ser,ts,tau,dim,save=False,save_loc='N',delaytau=0,mode='single',scaling='None'):
"""
Make Takens delay vectors.
Args:
ser: The time series (array) or (dataFrame) if mode is 'multivariate'.
ts: The time series name (string)
tau: The time delay of the time series (int). If mode is 'multivariate' it is an array of delays.
dim: The dimensionality of the time series (int). If mode is 'multivariate' it is an array of delays.
save_loc: The location to save the delay vectors (string).
save: A boolean to save the delay vectors (boolean).
delaytau: Eliminate the initial x values of the time series for equilibration (int).
mode: The mode of the time series ('single', 'multivariate', 'multitemporal') (string).
scaling: The scaling of the time series ('None', 'standard', 'return') (string).
Returns:
array: Single variate Takens delay vectors of shape (len(ser)-(dim-1)*tau, dim).
"""
if mode=='single':
ser = ser[delaytau::]
print(len(ser))
delayVecs = np.reshape(ser[0:len(ser)-(dim-1)*tau],(len(ser)-(dim-1)*tau,1))
for ii in range(1,dim):
delayVecs = np.concatenate((delayVecs,np.reshape(ser[ii*tau:len(ser)-(dim-1)*tau+ii*tau],(len(ser)-(dim-1)*tau,1))), axis=1)
#standard deviation scaling of delay vectors
if scaling=='standard':
for kk in range(delayVecs.shape[0]):
delayVecs[kk,:] = (delayVecs[kk,:] - np.mean(delayVecs[kk,:]))/np.std(delayVecs[kk,:])
#returns scaling of delay vectors
elif scaling=='return':
for kk in range(delayVecs.shape[0]):
# each row has each position subtracted by the first position for return and divided by the mean value of the row
mval = np.mean(delayVecs[kk,:])
delayVecs[kk,:] = np.divide((np.subtract(delayVecs[kk,:],delayVecs[kk,0])),mval)
if mode=='multivariate': #let ser is a dataframe
iii = 0
#reshape the portion of ser accessible to takens of dim*tau
for serz in ser.columns:
if isinstance(tau,int)==False:
tauv = tau[iii]
dimv = dim[iii]
#define which ts to use
serp = np.array(ser[serz])[delaytau:]
#append all vals to each element in dvecs as in Cao 1998
if iii==0:
delayVecs = [[] for x in range(len(serp)-(dimv-1)*tauv)]
dvecs_mult = np.reshape(serp[0:len(serp)-(dimv-1)*tauv],(len(serp)-(dimv-1)*tauv,1))
for ii in range(1,dimv):
dvecs_mult = np.concatenate((dvecs_mult,np.reshape(serp[ii*tauv:len(serp)-(dimv-1)*tauv+ii*tauv],(len(serp)-(dimv-1)*tauv,1))), axis=1)
#standard deviation scaling of delay vectors
if scaling=='standard':
for kk in range(delayVecs.shape[0]):
dvecs_mult[kk,:] = (dvecs_mult[kk,:] - np.mean(dvecs_mult[kk,:]))/np.std(dvecs_mult[kk,:])
#returns scaling of delay vectors
elif scaling=='return':
for kk in range(dvecs_mult.shape[0]):
# each row has each position subtracted by the first position for return and divided by the mean value of the row
mval = np.mean(dvecs_mult[kk,:])
dvecs_mult[kk,:] = np.divide((np.subtract(dvecs_mult[kk,:],dvecs_mult[kk,0])),mval)
#combine delay vectors
if iii>=1:
mlen= int(min(len(dvecs_mult),len(delayVecs)))
else:
mlen = len(dvecs_mult)
delayVecs= np.hstack((delayVecs[0:mlen],dvecs_mult[0:mlen]))
iii+=1
if mode=='multitemporal': #let ser is an array, tau is an array of delays, dim is dimension
#max dvec size:
ser = ser[delaytau:]
print('max size is nVecs = %d' %(len(ser)-(dim-1)*max(tau)))
#initialize
delayVecs = [[] for x in range(len(ser)-(dim-1)*tau[0])]
iii = 0
#reshape the portion of ser accessible to takens of dim*tau
for tauv in tau:
# dvecs_mult = []
#append all vals to each element in dvecs as in Cao 1998
#make dvecs for each given tau
dvecs_mult = np.reshape(ser[0:len(ser)-(dim-1)*tauv],(len(ser)-(dim-1)*tauv,1))
#make dvecs for each observation
for ii in range(1,dim):
dvecs_mult = np.concatenate((dvecs_mult,np.reshape(ser[ii*tauv:len(ser)-(dim-1)*tauv+ii*tauv],(len(ser)-(dim-1)*tauv,1))), axis=1)
#standard deviation scaling of delay vectors
if scaling=='standard':
for kk in range(delayVecs.shape[0]):
dvecs_mult[kk,:] = (dvecs_mult[kk,:] - np.mean(dvecs_mult[kk,:]))/np.std(dvecs_mult[kk,:])
#returns scaling of delay vectors
elif scaling=='return':
for kk in range(dvecs_mult.shape[0]):
# each row has each position subtracted by the first position for return and divided by the mean value of the row
mval = np.mean(dvecs_mult[kk,:])
dvecs_mult[kk,:] = np.divide((np.subtract(dvecs_mult[kk,:],dvecs_mult[kk,0])),mval)
#compute min length of dvec to make sure we can concatenate
if iii>=1:
mlen= int(min(len(dvecs_mult),len(delayVecs)))
#set to whatever first dvec is otherwise
else:
mlen = len(dvecs_mult)
#hstack concatenated dvecs as per Cao 1998
delayVecs= np.hstack((delayVecs[0:mlen],dvecs_mult[0:mlen]))
iii+=1
# saving delay vectors
if save==True:
np.savez('%s/dvecs_%s_tau%d_dim%d.npz' %(save_loc,ts,tau,dim), dvecs=delayVecs)
return delayVecs
####################################################################################################################
#Diffusion map
#4.1: Pairwise distance matrix
def make_P(ts,dvecs,train,tau,dim,save=False,save_loc='N',issymmetric=False,distmetric='euclidean'):
"""
Compute the distance matrix of the time series.
Args:
ts: The time series name (string).
dvecs: The delay vectors of the time series (array).
train: The training data (array).
tau: The time delay of the time series (int).
dim: The dimensionality of the time series (int).
save_loc: The location to save the diffusion map (string).
save: A boolean to save the diffusion map (boolean).
issymmetric: A boolean to determine if the diffusion map is symmetric (boolean).
distmetric: The distance metric to use ('euclidean', 'manhattan', 'cosine') (string).
Returns:
array: The pairwise distance matrix of the time series.
"""
dvecs_ss = deepcopy(dvecs[train])
# euclidean distance between two vectors
if distmetric=='euclidean':
if issymmetric==False:
P = euclidean_distances(dvecs_ss,dvecs_ss)
else:
dvecs_ss_flip = np.flip(dvecs_ss,axis=0)
P = np.minimum( euclidean_distances(dvecs_ss,dvecs_ss), euclidean_distances(dvecs_ss_flip,dvecs_ss) )
# manhattan distance between two vectors
elif distmetric=='manhattan':
if issymmetric==False:
P = manhattan_distances(dvecs_ss,dvecs_ss)
else:
dvecs_ss_flip = np.flip(dvecs_ss,axis=0)
P = np.minimum( manhattan_distances(dvecs_ss,dvecs_ss), manhattan_distances(dvecs_ss_flip,dvecs_ss) )
#cosine similarity of two vectors
elif distmetric=='cosine':
if issymmetric==False:
P = cosine_distances(dvecs_ss,dvecs_ss)
else:
dvecs_ss_flip = np.flip(dvecs_ss,axis=0)
P = np.minimum( cosine_distances(dvecs_ss,dvecs_ss), cosine_distances(dvecs_ss_flip,dvecs_ss) )
if save==True:
np.savez('%s/P_%s_tau%d_dim%d.npz' %(save_loc,ts,tau,dim), P=P)
return P
####################################################################################################################
# 4.2: Gaussian Kernel Width Search
def eps_scan(ts,tau,dim,P,dvecs,train,ll=-10,ul=10,res=0.5,tol=0.85,save=False,save_loc='N',plots=False,return_all=False,nEvals=25,i_dim=7,method='linear',fit=2,linlim='Minlinlim',clim=20,mineps=-1,lamulim=0.9,lamlim=0.3,lamllim=0.1):
"""
Compute the optimal epsilon value for the diffusion map.
Choice for EPS can be made such that subsequent dMap has dimension equal to E1d analysis result OR
condition listed on page 6 of DOI: 10.1109/TPAMI.2008.76 (algorithm 3.1)for kernel selection as default
note also that we choose the inflection point as the center of the linear region of the log(sumA) vs log(eps) curve wheras the
condition listed in the paper is that epsilon be chosen from the linear region [epsilon_min,epsilon_max].
Args:
ts: The time series name (string).
tau: The time delay of the time series (int).
dim: The dimensionality of the time series (int).
P: The distance matrix of the time series (array).
dvecs: The delay vectors of the time series (array).
train: The training data (array).
ll: The lower limit of the epsilon scan (float).
ul: The upper limit of the epsilon scan (float).
res: The resolution of the epsilon scan (float).
tol: The tolerance for the epsilon scan (float).
save_loc: The location to save the epsilon scan (string).
save: A boolean to save the epsilon scan (boolean).
plots: A boolean to plot the epsilon scan (boolean).
return_all: A boolean to return all epsilon values (boolean).
nEvals: The number of eigenvalues to compute (int).
i_dim: The intrinsic dimensionality of the data computed from E1d (int).
method: The method to determine epsilon ('manual', 'linear', 'intrinsic') (string).
fit: Numbers of points to use for the fit for the epsilon scan (int).
linlim: The limit for the epsilon scan ('Maxlinlim', 'Minlinlim') (string).
clim: The number of attempts to find the epsilon value (int).
mineps: The minimum epsilon value for the epsilon scan (float).
lamulim: The upper limit for the epsilon scan (float).
lamlim: The limit for the epsilon scan (float).
lamllim: The lower limit for the epsilon scan (float).
Returns:
float: The optimal epsilon value for the diffusion map.
"""
N = P.shape[0]
log_eps_array = np.arange(ll,ul,res)
sumA = np.empty_like(log_eps_array)
for i in range(len(log_eps_array)):
eps = np.exp(log_eps_array[i])
sumA[i] = np.sum(np.exp(-P**2/(2*eps)),axis=None)
if method=='manual':
for ij in range(len(sumA)):
i = np.log(sumA[ij])
if i >= (np.log(sumA[0])+np.log(sumA[-1]))/2: #(np.log(N) - np.log(N)*tol):
epsval = log_eps_array[ij]
break
elif ij==len(sumA)-1:
print('no epsval found, investigate range choice')
print('See DOI: 10.1109/TPAMI.2008.76 P7-9 for kernel selection criteria')
epsval = ul
#compare with L method
epsval = log_eps_array[indx]
P = make_P(ts,dvecs,train,tau,dim)
#compute dMap to get eval spectrum for restricted dataset
lamb,psi,eps = dMap(ts,tau,dim,P,epsval,nEvals)
tdim = L_method(lamb,plots=True)
print('Implied dimensionality: ', tdim)
#Verlet 2nd derivative
elif method=='linear':
SD = []
for ij in range(1,len(sumA)-1):
# take first derivative
#estimate of SD
SD.append((np.log(sumA[1+ij])+np.log(sumA[ij-1])-2*np.log(sumA[ij]))/res**2)
plt.scatter(range(len(SD)),SD)
plt.xlabel('eps index')
plt.ylabel('second derivative')
plt.xticks(np.arange(0,len(SD),1),log_eps_array[1:-1])
plt.title('Second Derivative of log(sumA) vs log(eps)')
plt.show()
plt.close()
#check sign change
#iterate over len SD to find inflection point
#why argmax? In the case where there are two linear regions we want the region of largest difference
print(np.argmax(SD),SD[np.argmax(SD)] )
for ij in range(np.argmax(SD)-2,len(SD)):
# double check if second derivative is positive and then negative for confirmed inflection point
if SD[ij-1]>=0 and SD[ij]<=0:
indx = ij+1-fit #why? +1 for the SD because we want to start on the far left of the linear region and work our way to correct dim
epsval = log_eps_array[indx]
break
else:
indx = ij
if linlim=='Maxlinlim':
epsval = log_eps_array[indx+fit]
elif linlim=='Minlinlim':
epsval = log_eps_array[indx-fit]
#compare with L method
epsval = log_eps_array[indx]
P = make_P(ts,dvecs,train,tau,dim)
#compute dMap to get eval spectrum for restricted dataset
lamb,psi,eps = dMap(ts,tau,dim,P,epsval,nEvals)
tdim = L_method(lamb,plots=True)
print('Implied dimensionality: ', tdim)
#match the intrinsic dimensionality of the data as per DOI: 10.1109/TPAMI.2008.76 manifold learning choice
# L method of Salvador and Chan - https://cs.fit.edu/~pkc/papers/ictai04salvador.pdf
elif method=='intrinsic':
#second derivative: find point of inflection where rate of change of slope goes from positive to negative
SD = []
for ij in range(1,len(sumA)-1):
# take first derivative
#estimate of SD
SD.append((np.log(sumA[1+ij])+np.log(sumA[ij-1])-2*np.log(sumA[ij]))/res**2)
plt.figure(figsize=(20,10))
plt.scatter(range(len(SD)),SD)
plt.xlabel('eps index')
plt.ylabel('second derivative')
plt.xticks(np.arange(0,len(SD),1),log_eps_array[1:-1])
plt.title('Second Derivative of log(sumA) vs log(eps)')
plt.show()
plt.close()
#iterate over len SD to find inflection point
#why argmax? In the case where there are two linear regions we want the region of largest difference
for ij in range(1,len(SD)):
# double check if second derivative is positive and then negative for confirmed inflection point
if SD[ij-1]>=0 and SD[ij]<=0:
indx = ij+1-fit #why? +1 for the SD because we want to start on the far left of the linear region and work our way to correct dim
break
#if we never get to inflection point:
elif ij==len(SD)-1:
print('No epsval found, investigate range choice. Increase range and/or resolution')
print('See DOI: 10.1109/TPAMI.2008.76 P7-9 for kernel selection criteria')
print('Displaying spectrum')
fig, ax = plt.subplots()
ax.scatter(log_eps_array, np.log(sumA))
ax.plot([log_eps_array[0],log_eps_array[-1]],[np.log(N**2),np.log(N**2)],'k:')
ax.plot([log_eps_array[0],log_eps_array[-1]],[np.log(N),np.log(N)],'k:')
ax.set_xlabel("log(eps)")
ax.set_ylabel("log(sumA)")
plt.show()
plt.close()
indx = ij
#first guess at correct epsilon; we use - fit to ensure we begin in the over saturated region
epsval = log_eps_array[indx]
if epsval <mineps:
epsval = mineps
P = make_P(ts,dvecs,train,tau,dim)
#compute dMap to get eval spectrum for restricted dataset
lamb,psi,eps = dMap(ts,tau,dim,P,epsval,nEvals)
tdim = L_method(lamb,plots=True)
#we check if the extracted dimensionality is smaller than the intrinsic dim from E1d
#if yes, we augment the gaussian kernel estimation and recompute the eigenvalue spectrum
#it is recommended to use plots=True to see the spectrum and the linear fit
val = 1
mem = [0]
#while tdim - i_dim > 1 and np.mean(lamb[1:3])>0.9 and lamb[1]>0.3 and val < 20 or np.mean(lamb[10:15])>0.3: #should not be converged. if converged, stop mem[-1]!=mem[-2]
while val < clim:
if tdim - i_dim > 1 and np.mean(lamb[1:3])>lamulim and lamb[1]>lamlim or np.mean(lamb[12:20])>lamllim:
try: # to deal with convergence issues leading to linalg error, we try and
epsval = log_eps_array[indx+val]
if log_eps_array[indx+val]<=mineps:
epsval = mineps
#accelerate sampling
if np.mean(lamb[1:15])>lamulim:
val +=2
epsval = log_eps_array[indx+val]
print('epsval we are trying: ', epsval)
P = make_P(ts,dvecs,train,tau,dim)
lamb,psi,eps = dMap(ts,tau,dim,P,epsval,nEvals)
tdim = L_method(lamb,plots)
mem.append(tdim)
val+=1
print('max eval is: ', lamb[1])
except:
print('skipped epsval: ', log_eps_array[indx+val] )
val +=1
continue
#pass
else:
print('Convergence achieved, implied dimensionality: ', tdim)
print('mean eval is: ', np.mean(lamb[1:15]))
print(mem)
break
if plots==True:
fig, ax = plt.subplots()
ax.scatter(log_eps_array, np.log(sumA))
ax.plot([log_eps_array[0],log_eps_array[-1]],[np.log(N**2),np.log(N**2)],'k:')
ax.plot([log_eps_array[0],log_eps_array[-1]],[np.log(N),np.log(N)],'k:')
ax.set_xlabel("log(eps)")
ax.set_ylabel("log(sumA)")
fig.savefig('%s/eps_%s_tau%d_dim%d.png' %(save_loc,ts,tau,dim), dpi=300)
#straight line fit: - let slope be the first derivative at the inflection point
slope = (np.log(sumA[indx+fit])-np.log(sumA[indx-fit]))/(2*fit*res)
intersec = np.log(sumA[indx])-slope*log_eps_array[indx]
ax.plot(log_eps_array[indx-fit:indx+fit+1], slope*log_eps_array[indx-fit:indx+fit+1]+intersec,color='red')
if save==True:
np.savez('%s/eps_%s_tau%d_dim%d.npz' %(save_loc,ts,tau,dim), epsval=epsval)
# DOI: 10.1109/TPAMI.2008.76
#apply the linearity condition
return epsval
####################################################################################################################
# 4.3: Diffusion Map
def dMap(ts,tau,dim,P,epsval,nEvals,save=False,save_loc='/N',plots=False,plot_stride=100):
"""
Compute the diffusion map from a pairwise distance matrix.
Args:
ts: The time series name (string).
tau: The time delay of the time series (int).
dim: The dimensionality of the time series (int).
P: The distance matrix of the time series (array).
epsval: The optimal epsilon value for the diffusion map (float).
nEvals: The number of eigenvalues to compute (int).
save_loc: The location to save the diffusion map (string).
save: A boolean to save the diffusion map (boolean).
plots: A boolean to plot the diffusion map (boolean).
plot_stride: The stride of the diffusion map plot (int).
Returns:
array: The eigenvalues of the diffusion map (array).
array: The eigenvectors of the diffusion map (array).
float: The optimal epsilon value for the diffusion map (float).
"""
eps = np.exp(epsval)
P = np.exp(-P**2/(2*eps))
D = np.sum(P,axis=1)
P = np.matmul(np.matmul(np.diag(D**(-0.5)),P),np.diag(D**(-0.5))) # constructing Ms as symmetric matrix for diagonalization
lamb, psi = eigh(P,subset_by_index=(P.shape[0]-nEvals,P.shape[0]-1)) # scipy eigh to specify only computation of leading evals
print(lamb.shape)
psi = np.matmul(np.diag(D**(-0.5)),psi) # converting evecs of Ms to evecs of M; M and Ms share evals
idx_sort = np.flip(np.argsort(lamb))
lamb,psi = lamb[idx_sort],psi[:,idx_sort]
if save==True:
np.savez('%s/map_%s_tau%d_dim%d.npz' %(save_loc,ts,tau,dim), lamb=lamb, psi=psi)
if plots==True:
#plot
fig = plt.figure()
ax = fig.add_subplot(111)
ax.bar(np.arange(1,len(lamb)), lamb[1:])
ax.set_xlabel('Eigenvalue index',fontsize=15)
ax.set_ylabel('Eigenvalues',fontsize=15)
ax.set_title('Eigenvalue Spectrum',fontsize=15)
plt.xticks(np.arange(1, len(lamb), step=1))
plt.tight_layout()
fig.savefig('%s/evals_eps_%s_tau%d_dim%d.png' %(save_loc,ts,tau,dim), dpi=300)
plt.show()
plt.close()
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
i,j,k=1,2,3
ax.scatter(psi[::plot_stride,i], psi[::plot_stride,j], psi[::plot_stride,k])
ax.set_xlabel(r'$\Psi_%s$' %str(i),fontsize=15)
ax.set_ylabel(r'$\Psi_%s$' %str(j),fontsize=15)
ax.set_zlabel(r'$\Psi_%s$' %str(k),fontsize=15)
ax.set_title('Diffusion Map Embedding',fontsize=15)
plt.tight_layout()
plt.draw()
plt.show()
plt.close()
plt.savefig('%s/dMap_%s_tau%d_dim%d.pdf' %(save_loc,ts,tau,dim))
return lamb,psi,eps
#####################################################################################################################
# 4.4: Intrinsic Dimensionality
def L_method(lamb,plots=False):
"""
Estimate the intrinsic dimensionality of the data from the eigenvalue spectrum
Args:
lamb: The eigenvalues of the diffusion map (array).
plots: A boolean to plot the intrinsic dimensionality (boolean).
Returns:
int: The intrinsic dimensionality of the data (int).
"""
TRMSE = []
lamb = lamb[1:]
lamb_indx = np.linspace(1,len(lamb),len(lamb))#[0:-1]
for ij in range(1,len(lamb)-1):
l_fit1 = np.polyfit(lamb_indx[0:ij+1],lamb[0:ij+1],1)
l_fit2 = np.polyfit(lamb_indx[ij:],lamb[ij:],1)
#compute RMSEs for each fit with the data
RMSE1 =(lamb[0:ij+1]-np.polyval(l_fit1,lamb_indx[0:ij+1]))
RMSE2 = (lamb[ij:]-np.polyval(l_fit2,lamb_indx[ij:]))
RMSE3 = np.array(list(RMSE1)+list(RMSE2))
TRMSE.append(np.sqrt(np.mean(RMSE3**2)))
#find minimum TRMSE
#why +1? - because we are indexing from 1. Still need to add 1 for intrinsic dim in labeling
true_dim = int(lamb_indx[np.argmin(TRMSE)])
if plots==True:
l_fit1 = np.polyfit(lamb_indx[0:true_dim+1],lamb[0:true_dim+1],1)
l_fit2 = np.polyfit(lamb_indx[true_dim:],lamb[true_dim:],1)
l1 = np.polyval(l_fit1,lamb_indx)
l2 = np.polyval(l_fit2,lamb_indx)
l1 = l1[l1>-0.2]
#plot
fig = plt.figure(figsize=(10,5))
ax = fig.add_subplot(111)
ax.bar(np.arange(1,len(lamb)+1), lamb)
ax.plot(range(1,len(l1)+1),l1)
ax.plot(range(1,len(l2)+1),l2)
ax.set_xlabel('Eigenvalue index',fontsize=15)
ax.set_ylabel('Eigenvalues',fontsize=15)
ax.set_title('L Method implies data is %d dimensional' %(true_dim+1),fontsize=15)
plt.xticks(np.arange(1, len(lamb)+1, step=1))
plt.tight_layout()
plt.show()
plt.close()
return true_dim+1
####################################################################################################################
# 4.5: Nystrom Extension
def nystrom(ts,tau,dim,lamb,psi,dvecs,train,eps,save_loc='N',save=True,plots=False,plot_stride=100):
"""
Compute the Nystrom extension of the diffusion map.
Args:
ts: The time series name (string).
tau: The time delay of the time series (int).
dim: The dimensionality of the time series (int).
lamb: The eigenvalues of the diffusion map (array).
psi: The eigenvectors of the diffusion map (array).
dvecs: The delay vectors of the time series (array).
train: The training data (array).
eps: The optimal epsilon value for the diffusion map (float).
save_loc: The location to save the Nystrom extension (string).
save: A boolean to save the Nystrom extension (boolean).
plots: A boolean to plot the Nystrom extension (boolean).
plot_stride: The stride of the Nystrom extension plot (int).
Returns:
array: The Nystrom extension of the diffusion map vectors.
"""
dvecs_ss = dvecs[train]
z = []
for i in range(dvecs.shape[0]):
Euc_i = euclidean_distances(dvecs_ss,dvecs[i].reshape(1,-1))
Euc_i = np.exp(-Euc_i**2/(2*eps))
Euc_i /= np.sum(Euc_i)
psi_Nystrom = np.divide( np.matmul(Euc_i.T,psi), lamb)
z.append(psi_Nystrom)
z = np.array(z)
z = z.reshape(z.shape[0],-1)
# saving dMap embedding of all data
np.savez('%s/data_%s_tau%d_dim%d.npz' %(save_loc,ts,tau,dim), z=z)
if plots==True:
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
i,j,k=1,2,3
ax.scatter(psi[::plot_stride,i], psi[::plot_stride,j], psi[::plot_stride,k])
ax.set_xlabel(r'$\Psi_%s$' %str(i),fontsize=15)
ax.set_ylabel(r'$\Psi_%s$' %str(j),fontsize=15)
ax.set_zlabel(r'$\Psi_%s$' %str(k),fontsize=15)
ax.set_title('Nystrom Extension of Embedding',fontsize=15)
plt.tight_layout()
plt.draw()
plt.show()
plt.close()
plt.savefig('%s/nystrom_%s_tau%d_dim%d.pdf' %(save_loc,ts,tau,dim))
return z
####################################################################################################################
#Loading Functions
# 5.1: Loading Manifolds
def load_manif(load_loc,n_features,first_feature=0,z='empty'):
"""
Load a manifold from an npz file and return the features we wish to use.
Args: