11
11
import itertools
12
12
from queue import PriorityQueue
13
13
14
- import sortedcontainers
15
14
from infinity import inf
15
+ from sortedcontainers import SortedDict
16
16
17
17
from . import histogram , operations , plot , utils
18
18
@@ -47,9 +47,8 @@ class TimeSeries:
47
47
"""
48
48
49
49
def __init__ (self , data = None , default = None ):
50
- self ._d = sortedcontainers . SortedDict (data )
50
+ self ._d = SortedDict (data )
51
51
self .default = default
52
-
53
52
self .getter_functions = {
54
53
"previous" : self ._get_previous ,
55
54
"linear" : self ._get_linear_interpolate ,
@@ -66,43 +65,34 @@ def __setstate__(self, state):
66
65
67
66
def __iter__ (self ):
68
67
"""Iterate over sorted (time, value) pairs."""
69
- return iter (self ._d . items ())
68
+ return iter (self .items ())
70
69
71
70
def __bool__ (self ):
72
71
return bool (self ._d )
73
72
74
73
def is_empty (self ):
75
74
return len (self ) == 0
76
75
77
- @property
78
- def default (self ):
79
- """Return the default value of the time series."""
80
- return self ._default
76
+ @staticmethod
77
+ def linear_interpolate (v0 , v1 , t ):
78
+ return v0 + t * (v1 - v0 )
81
79
82
- @default .setter
83
- def default (self , value ):
84
- """Set the default value of the time series."""
85
- self ._default = value
80
+ @staticmethod
81
+ def scaled_time (t0 , t1 , time ):
82
+ return (time - t0 ) / (t1 - t0 )
86
83
87
84
def _get_linear_interpolate (self , time ):
88
85
right_index = self ._d .bisect_right (time )
89
86
left_index = right_index - 1
90
- if left_index < 0 :
87
+ if left_index < 0 : # before first measurement
91
88
return self .default
92
- elif right_index == len (self ._d ):
93
- # right of last measurement
94
- return self .last_item ()[1 ]
89
+ elif right_index == len (self ._d ): # after last measurement
90
+ return self .last_value ()
95
91
else :
96
92
left_time , left_value = self ._d .peekitem (left_index )
97
93
right_time , right_value = self ._d .peekitem (right_index )
98
- dt_interval = right_time - left_time
99
- dt_start = time - left_time
100
- if isinstance (dt_interval , datetime .timedelta ):
101
- dt_interval = dt_interval .total_seconds ()
102
- dt_start = dt_start .total_seconds ()
103
- slope = (right_value - left_value ) / dt_interval
104
- value = slope * dt_start + left_value
105
- return value
94
+ t = self .scaled_time (left_time , right_time , time )
95
+ return self .linear_interpolate (left_value , right_value , t )
106
96
107
97
def _get_previous (self , time ):
108
98
right_index = self ._d .bisect_right (time )
@@ -197,6 +187,9 @@ def compact(self):
197
187
same at all times, but repeated measurements are discarded.
198
188
199
189
"""
190
+
191
+ # todo: change to to_compact, do not modify in place. mark as deprecated
192
+
200
193
previous_value = object ()
201
194
redundant = []
202
195
for time , value in self :
@@ -215,6 +208,9 @@ def exists(self):
215
208
otherwise
216
209
217
210
"""
211
+
212
+ # todo: this needs a better name. mark exists as deprecated
213
+
218
214
result = TimeSeries (default = self .default is not None )
219
215
for t , v in self :
220
216
result [t ] = v is not None
@@ -236,6 +232,9 @@ def remove_points_from_interval(self, start, end):
236
232
[start:end].
237
233
238
234
"""
235
+
236
+ # todo: consider whether this key error should be suppressed
237
+
239
238
for s , _e , _v in self .iterperiods (start , end ):
240
239
with contextlib .suppress (KeyError ):
241
240
del self ._d [s ]
@@ -251,6 +250,8 @@ def __len__(self):
251
250
def __repr__ (self ):
252
251
"""A detailed string representation for debugging."""
253
252
253
+ # todo: show default in string representation
254
+
254
255
def format_item (item ):
255
256
return "{!r}: {!r}" .format (* item )
256
257
@@ -265,6 +266,8 @@ def __str__(self):
265
266
266
267
"""
267
268
269
+ # todo: show default in string representation
270
+
268
271
def format_item (item ):
269
272
return "{!s}: {!s}" .format (* item )
270
273
@@ -309,6 +312,9 @@ def iterintervals(self, n=2):
309
312
310
313
@staticmethod
311
314
def _value_function (value ):
315
+ # todo: should this be able to take NotGiven, so that it would
316
+ # be possible to filter for None explicitly?
317
+
312
318
# if value is None, don't filter
313
319
if value is None :
314
320
@@ -333,9 +339,11 @@ def iterperiods(self, start=None, end=None, value=None):
333
339
"""This iterates over the periods (optionally, within a given time
334
340
span) and yields (interval start, interval end, value) tuples.
335
341
336
- TODO: add mask argument here.
337
-
338
342
"""
343
+
344
+ # todo: add mask argument here.
345
+ # todo: check whether this can be simplified with newer SortedDict
346
+
339
347
start , end , mask = self ._check_boundaries (
340
348
start , end , allow_infinite = False
341
349
)
@@ -647,7 +655,7 @@ def moving_average( # noqa: C901
647
655
648
656
@staticmethod
649
657
def rebin (binned , key_function ):
650
- result = sortedcontainers . SortedDict ()
658
+ result = SortedDict ()
651
659
for bin_start , value in binned .items ():
652
660
new_bin_start = key_function (bin_start )
653
661
try :
@@ -668,10 +676,10 @@ def bin(
668
676
transform = "distribution" ,
669
677
):
670
678
if mask is not None and mask .is_empty ():
671
- return sortedcontainers . SortedDict ()
679
+ return SortedDict ()
672
680
673
681
if start is not None and start == end :
674
- return sortedcontainers . SortedDict ()
682
+ return SortedDict ()
675
683
676
684
# use smaller if available
677
685
if smaller :
@@ -685,7 +693,7 @@ def bin(
685
693
start = utils .datetime_floor (start , unit = unit , n_units = n_units )
686
694
687
695
function = getattr (self , transform )
688
- result = sortedcontainers . SortedDict ()
696
+ result = SortedDict ()
689
697
dt_range = utils .datetime_range (start , end , unit , n_units = n_units )
690
698
for bin_start , bin_end in utils .pairwise (dt_range ):
691
699
result [bin_start ] = function (
@@ -898,11 +906,6 @@ def iter_merge(cls, timeseries_list):
898
906
if not timeseries_list :
899
907
return
900
908
901
- # for ts in timeseries_list:
902
- # if ts.is_floating():
903
- # msg = "can't merge empty TimeSeries with no default value"
904
- # raise KeyError(msg)
905
-
906
909
# This function mostly wraps _iter_merge, the main point of
907
910
# this is to deal with the case of tied times, where we only
908
911
# want to yield the last list of values that occurs for any
@@ -940,14 +943,6 @@ def merge(cls, ts_list, compact=True, operation=None):
940
943
result .set (t , value , compact = compact )
941
944
return result
942
945
943
- @staticmethod
944
- def csv_time_transform (raw ):
945
- return datetime .datetime .strptime (raw , "%Y-%m-%d %H:%M:%S" )
946
-
947
- @staticmethod
948
- def csv_value_transform (raw ):
949
- return str (raw )
950
-
951
946
@classmethod
952
947
def from_csv (
953
948
cls ,
@@ -959,11 +954,15 @@ def from_csv(
959
954
skip_header = True ,
960
955
default = None ,
961
956
):
957
+ # todo: allowing skipping n header rows
958
+
962
959
# use default on class if not given
963
960
if time_transform is None :
964
- time_transform = cls .csv_time_transform
961
+ time_transform = lambda s : datetime .datetime .strptime (
962
+ s , "%Y-%m-%d %H:%M:%S"
963
+ )
965
964
if value_transform is None :
966
- value_transform = cls . csv_value_transform
965
+ value_transform = lambda s : s
967
966
968
967
result = cls (default = default )
969
968
with open (filename ) as infile :
@@ -976,7 +975,7 @@ def from_csv(
976
975
result [time ] = value
977
976
return result
978
977
979
- def operation (self , other , function , ** kwargs ):
978
+ def operation (self , other , function , default = None ):
980
979
"""Calculate "elementwise" operation either between this TimeSeries
981
980
and another one, i.e.
982
981
@@ -992,7 +991,11 @@ def operation(self, other, function, **kwargs):
992
991
constant, the measurement times will not change.
993
992
994
993
"""
995
- result = TimeSeries (** kwargs )
994
+
995
+ # todo: consider the best way to deal with default, and make
996
+ # consistent with other methods. check to_bool maybe
997
+
998
+ result = TimeSeries (default = default )
996
999
if isinstance (other , TimeSeries ):
997
1000
for time , value in self :
998
1001
result [time ] = function (value , other [time ])
@@ -1047,6 +1050,10 @@ def threshold(self, value, inclusive=False):
1047
1050
inclusive=True).
1048
1051
1049
1052
"""
1053
+
1054
+ # todo: this seems like it's wrong... make a test to check (and fix if so!)
1055
+ # todo: deal with default
1056
+
1050
1057
if inclusive :
1051
1058
1052
1059
def function (x , y ):
@@ -1061,6 +1068,9 @@ def function(x, y):
1061
1068
1062
1069
def sum (self , other ):
1063
1070
"""sum(x, y) = x(t) + y(t)."""
1071
+
1072
+ # todo: better consistency and documentation about when Nones are ignored
1073
+
1064
1074
return TimeSeries .merge (
1065
1075
[self , other ], operation = operations .ignorant_sum
1066
1076
)
0 commit comments