time_series_math 0.1.1
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.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +64 -0
- data/Rakefile +10 -0
- data/examples/ts_benchmark.rb +75 -0
- data/ext/time_series_math_c/extconf.rb +4 -0
- data/ext/time_series_math_c/time_series_math_c.c +80 -0
- data/lib/time_series_math.rb +11 -0
- data/lib/time_series_math/elemwise_operators.rb +56 -0
- data/lib/time_series_math/linear_interpolation.rb +16 -0
- data/lib/time_series_math/time_series.rb +167 -0
- data/lib/time_series_math/version.rb +3 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/time_series_math/elemwise_operators_spec.rb +44 -0
- data/spec/time_series_math/linear_interpolation_spec.rb +32 -0
- data/spec/time_series_math/time_series_math_c_spec.rb +44 -0
- data/spec/time_series_math/time_series_spec.rb +97 -0
- data/spec/time_series_math/time_series_use_api_spec.rb +16 -0
- data/spec/time_series_math_spec.rb +5 -0
- data/time_series_math.gemspec +27 -0
- metadata +131 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 85c802a00527e11700194fcbd661d85943bcd26a
|
4
|
+
data.tar.gz: 4cd75e47704fbecd1557cba39cb6fa44f32e29f7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 83e1bb3b5dc55d5f83bfb8ad6abe9d7f8b14f3f2a4acdadc9717968f699d425d7b966d81b5507adf443eb080e5434d939a237195de9daaf007cad565ed808e78
|
7
|
+
data.tar.gz: fafb01c6289d5065f7dab7ee7a33f2f8e5e63b7cd88a0cdab343c6a1833ef1a4c8ff26f0b77ce70ce075598865d035e392898b24332b9caf41d9b9b145ab94af
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Alex Kukushkin
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
# TimeSeriesMath
|
2
|
+
|
3
|
+
Time series data structures and functions.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'time_series_math'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install time_series_math
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
### TimeSeries
|
22
|
+
|
23
|
+
`TimeSeries` class is a data structure for storing timestamped values.
|
24
|
+
|
25
|
+
TimeSeries maintains order of its elements and provides efficient search methods
|
26
|
+
for near-constant time access
|
27
|
+
(depends strongly on timestamps distribution -- the more even, the better).
|
28
|
+
|
29
|
+
Examples:
|
30
|
+
|
31
|
+
``` ruby
|
32
|
+
require 'time_series_math'
|
33
|
+
|
34
|
+
include TimeSeriesMath
|
35
|
+
|
36
|
+
# one by one element insertion:
|
37
|
+
ts = TimeSeries.new
|
38
|
+
ts.push 1.0, { x: 2.0, y: 3.0 }
|
39
|
+
ts.push 1.2, { x: 2.0, y: 3.0 }
|
40
|
+
ts.push 1.6, { x: 2.0, y: 3.0 }
|
41
|
+
ts.push 1.9, { x: 2.0, y: 3.0 }
|
42
|
+
ts.push 2.1, { x: 2.0, y: 3.0 }
|
43
|
+
# .. or:
|
44
|
+
ts[2.3] = { x: 2.5, y: 3.5 }
|
45
|
+
ts[2.5] = { x: 2.2, y: 3.7 }
|
46
|
+
|
47
|
+
# more time-efficient batch insertion using arrays:
|
48
|
+
ts = TimeSeries.new
|
49
|
+
tt = [ 1.0, 1.2, 1.6, 1.9, 2.1 ]
|
50
|
+
dd = [ {x: 2.0}, {x: 2.1}, {x: 2.5}, {x: 2.7}, {x: 2.85} ]
|
51
|
+
ts.push_array(tt, dd)
|
52
|
+
|
53
|
+
# retrieve closest element before given time:
|
54
|
+
ts[1.2] # => { x: 2.1 }
|
55
|
+
ts[2.095] # => { x: 2.7 }
|
56
|
+
```
|
57
|
+
|
58
|
+
## Contributing
|
59
|
+
|
60
|
+
1. Fork it ( https://github.com/kukushkin/time_series_math/fork )
|
61
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
62
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
63
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
64
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
RSpec::Core::RakeTask.new(:spec)
|
5
|
+
|
6
|
+
require 'rake/extensiontask'
|
7
|
+
spec = Gem::Specification.load('time_series_math.gemspec')
|
8
|
+
Rake::ExtensionTask.new('time_series_math_c', spec)
|
9
|
+
|
10
|
+
task spec: :compile
|
@@ -0,0 +1,75 @@
|
|
1
|
+
$LOAD_PATH.unshift '../lib'
|
2
|
+
require 'time_series_math'
|
3
|
+
require 'benchmark'
|
4
|
+
|
5
|
+
include TimeSeriesMath
|
6
|
+
|
7
|
+
def t_rand
|
8
|
+
rand * 1000.0
|
9
|
+
end
|
10
|
+
|
11
|
+
def b_search(ts, t)
|
12
|
+
return nil if t < ts.t_first
|
13
|
+
return ts.last if t >= ts.t_last
|
14
|
+
ileft = 0
|
15
|
+
iright = ts.size-1
|
16
|
+
while iright - ileft > 1
|
17
|
+
icenter = ileft + (iright - ileft) /2
|
18
|
+
if t >= ts.data[icenter][0]
|
19
|
+
ileft = icenter
|
20
|
+
else
|
21
|
+
iright = icenter
|
22
|
+
end
|
23
|
+
end
|
24
|
+
if iright - ileft != 1
|
25
|
+
fail "ileft:#{ileft} iright:#{iright}"
|
26
|
+
elsif ts.data[ileft][0] > t || ts.data[iright][0] <= t
|
27
|
+
puts "(ts: t_first:#{ts.t_first} t_last:#{ts.t_last}"
|
28
|
+
fail "t:#{t}, t_left:#{ts.data[ileft][0]}, t_right:#{ts.data[iright][0]}"
|
29
|
+
end
|
30
|
+
v = ts.data[ileft]
|
31
|
+
end
|
32
|
+
|
33
|
+
def b_search_native(ts, t)
|
34
|
+
v_right = ts.data.bsearch { |d| d[0] > t }
|
35
|
+
v_right.nil? ? ts.last : v_right[1]
|
36
|
+
end
|
37
|
+
|
38
|
+
TS_N = [1_000, 100_000, 1_000_000]
|
39
|
+
TESTS = 1000_000
|
40
|
+
ts_list = []
|
41
|
+
|
42
|
+
TS_N.each do |num_samples|
|
43
|
+
puts "** initializing time series with #{num_samples} samples"
|
44
|
+
tt = []
|
45
|
+
dd = []
|
46
|
+
num_samples.times do
|
47
|
+
tt << t_rand
|
48
|
+
dd << { x: rand * 10.0 }
|
49
|
+
end
|
50
|
+
ts_list << TimeSeries.new(tt, dd)
|
51
|
+
end
|
52
|
+
|
53
|
+
puts '** running benchmarks'
|
54
|
+
Benchmark.bmbm do |b|
|
55
|
+
ts_list.each do |ts|
|
56
|
+
b.report "size: #{ts.size}, running #{TESTS} times #indices_at" do
|
57
|
+
TESTS.times { ts.indices_at(t_rand) }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
ts_list.each do |ts|
|
61
|
+
b.report "size: #{ts.size}, running #{TESTS} times #bsearch_indices_at" do
|
62
|
+
TESTS.times { ts.bsearch_indices_at(t_rand) }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
ts_list.each do |ts|
|
66
|
+
b.report "size: #{ts.size}, running #{TESTS} times #b_search" do
|
67
|
+
TESTS.times { b_search(ts, t_rand) }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
ts_list.each do |ts|
|
71
|
+
b.report "size: #{ts.size}, running #{TESTS} times #b_search_native" do
|
72
|
+
TESTS.times { b_search_native(ts, t_rand) }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
#include <ruby.h>
|
2
|
+
|
3
|
+
// Returns timestamp at index +i+.
|
4
|
+
//
|
5
|
+
static double timestamp_at_i(VALUE rb_data_array, int i ) {
|
6
|
+
VALUE m_elem;
|
7
|
+
|
8
|
+
m_elem = rb_ary_entry(rb_data_array, i);
|
9
|
+
return NUM2DBL( rb_ary_entry(m_elem, 0) );
|
10
|
+
}
|
11
|
+
|
12
|
+
// Returns indices of elements surrounding timestamp +t+.
|
13
|
+
// The pair of indices is found using binary search over sorted @data array.
|
14
|
+
//
|
15
|
+
// Ruby equivalent:
|
16
|
+
//
|
17
|
+
// def indices_at(t)
|
18
|
+
// return [nil, nil] if size == 0
|
19
|
+
// return [nil, 0] if t < t_first
|
20
|
+
// return [size - 1, nil] if t >= t_last
|
21
|
+
// ileft = 0
|
22
|
+
// iright = size - 1
|
23
|
+
// while iright - ileft > 1
|
24
|
+
// icenter = ileft + (iright - ileft) / 2
|
25
|
+
// if t >= data[icenter][0]
|
26
|
+
// ileft = icenter
|
27
|
+
// else
|
28
|
+
// iright = icenter
|
29
|
+
// end
|
30
|
+
// end
|
31
|
+
// [ileft, iright]
|
32
|
+
// end
|
33
|
+
//
|
34
|
+
static VALUE time_series_bsearch_indices_at(VALUE rb_self, VALUE rb_t) {
|
35
|
+
VALUE m_data;
|
36
|
+
int _size;
|
37
|
+
double _t;
|
38
|
+
int _ileft, _iright, _icenter;
|
39
|
+
|
40
|
+
m_data = rb_iv_get(rb_self, "@data");
|
41
|
+
_size = RARRAY_LEN(m_data);
|
42
|
+
_t = NUM2DBL(rb_t);
|
43
|
+
|
44
|
+
// initial conditions check:
|
45
|
+
if ( _size == 0 ) {
|
46
|
+
return rb_ary_new3( 2, Qnil, Qnil );
|
47
|
+
}
|
48
|
+
if ( _t < timestamp_at_i( m_data, 0 ) ) {
|
49
|
+
return rb_ary_new3( 2, Qnil, INT2FIX(0) );
|
50
|
+
}
|
51
|
+
if ( _t >= timestamp_at_i( m_data, _size-1 ) ) {
|
52
|
+
return rb_ary_new3( 2, INT2FIX(_size-1), Qnil );
|
53
|
+
}
|
54
|
+
|
55
|
+
// find left & right indices using binary search:
|
56
|
+
_ileft = 0;
|
57
|
+
_iright = _size - 1;
|
58
|
+
while ( _iright - _ileft > 1 ) {
|
59
|
+
_icenter = _ileft + ( _iright - _ileft ) / 2;
|
60
|
+
if ( _t >= timestamp_at_i( m_data, _icenter) ) {
|
61
|
+
_ileft = _icenter;
|
62
|
+
} else {
|
63
|
+
_iright = _icenter;
|
64
|
+
}
|
65
|
+
}
|
66
|
+
return rb_ary_new3( 2, INT2FIX(_ileft), INT2FIX(_iright) );
|
67
|
+
}
|
68
|
+
|
69
|
+
void Init_time_series_math_c() {
|
70
|
+
// get TimeSeriesMath module
|
71
|
+
ID sym_TimeSeriesMath = rb_intern("TimeSeriesMath");
|
72
|
+
VALUE mTimeSeriesMath = rb_const_get(rb_cObject, sym_TimeSeriesMath);
|
73
|
+
|
74
|
+
// get TimeSeriesMath::TimeSeries class
|
75
|
+
ID sym_TimeSeries = rb_intern("TimeSeries");
|
76
|
+
VALUE kTimeSeries = rb_const_get(mTimeSeriesMath, sym_TimeSeries);
|
77
|
+
|
78
|
+
// define TimeSeriesMath::TimeSeries#bsearch_indices_at
|
79
|
+
rb_define_method(kTimeSeries, "bsearch_indices_at", time_series_bsearch_indices_at, 1);
|
80
|
+
}
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'time_series_math/version'
|
2
|
+
require 'time_series_math/time_series'
|
3
|
+
require 'time_series_math/elemwise_operators'
|
4
|
+
require 'time_series_math/linear_interpolation'
|
5
|
+
|
6
|
+
# load C extensions
|
7
|
+
require 'time_series_math_c'
|
8
|
+
|
9
|
+
module TimeSeriesMath
|
10
|
+
# Your code goes here...
|
11
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module TimeSeriesMath
|
2
|
+
#
|
3
|
+
# == ElemwiseOperators
|
4
|
+
# A collection of helper functions for by-element operations:
|
5
|
+
# * addition
|
6
|
+
# * substraction
|
7
|
+
# * multiplication by scalar
|
8
|
+
#
|
9
|
+
module ElemwiseOperators
|
10
|
+
#
|
11
|
+
# Element-wise addition of objects
|
12
|
+
#
|
13
|
+
def elemwise_add(obj1, obj2)
|
14
|
+
case obj1
|
15
|
+
when Array
|
16
|
+
obj1.clone.zip(obj2).map { |d| d[0] + d[1] }
|
17
|
+
when Hash
|
18
|
+
out = {}
|
19
|
+
obj1.each { |k, v| out[k] = v + obj2[k] }
|
20
|
+
out
|
21
|
+
else
|
22
|
+
obj1 + obj2
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Element-wise substraction of objects
|
27
|
+
#
|
28
|
+
def elemwise_sub(obj1, obj2)
|
29
|
+
case obj1
|
30
|
+
when Array
|
31
|
+
obj1.clone.zip(obj2).map { |d| d[0] - d[1] }
|
32
|
+
when Hash
|
33
|
+
out = {}
|
34
|
+
obj1.each { |k, v| out[k] = v - obj2[k] }
|
35
|
+
out
|
36
|
+
else
|
37
|
+
obj1 - obj2
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Element-wise multiplication by scalar
|
42
|
+
#
|
43
|
+
def elemwise_mul_scalar(scalar, obj)
|
44
|
+
case obj
|
45
|
+
when Array
|
46
|
+
obj.clone.map { |d| d * scalar }
|
47
|
+
when Hash
|
48
|
+
out = {}
|
49
|
+
obj.each { |k, v| out[k] = v * scalar }
|
50
|
+
out
|
51
|
+
else
|
52
|
+
obj * scalar
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end # module ElemwiseOperators
|
56
|
+
end # module TimeSeriesMath
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module TimeSeriesMath
|
2
|
+
module LinearInterpolation
|
3
|
+
include ElemwiseOperators
|
4
|
+
# Returns interpolated value.
|
5
|
+
#
|
6
|
+
def [](t)
|
7
|
+
i0, i1 = indices_at(t)
|
8
|
+
return first[1] if i0.nil?
|
9
|
+
return last[1] if i1.nil?
|
10
|
+
|
11
|
+
k = (t - @data[i0][0]) / (@data[i1][0] - @data[i0][0])
|
12
|
+
diff_value = elemwise_sub(@data[i1][1], @data[i0][1])
|
13
|
+
elemwise_add(@data[i0][1], elemwise_mul_scalar(k, diff_value))
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end # module TimeSeriesMath
|
@@ -0,0 +1,167 @@
|
|
1
|
+
module TimeSeriesMath
|
2
|
+
#
|
3
|
+
# = TimeSeries
|
4
|
+
# TimeSeries class provides an efficient data structure for storing timestamped data values.
|
5
|
+
#
|
6
|
+
# TimeSeries maintains order of its elements and provides efficient search methods
|
7
|
+
# for near-constant time access
|
8
|
+
# (depends strongly on timestamps distribution -- the more even, the better).
|
9
|
+
#
|
10
|
+
# == Examples:
|
11
|
+
#
|
12
|
+
# require 'time_series_math'
|
13
|
+
# include TimeSeriesMath
|
14
|
+
#
|
15
|
+
# # one by one element insertion
|
16
|
+
# ts = TimeSeries.new
|
17
|
+
# ts.push 1.0, { x: 2.0, y: 3.0 }
|
18
|
+
# ts.push 1.2, { x: 2.0, y: 3.0 }
|
19
|
+
# ts.push 1.6, { x: 2.0, y: 3.0 }
|
20
|
+
# ts.push 1.9, { x: 2.0, y: 3.0 }
|
21
|
+
# ts.push 2.1, { x: 2.0, y: 3.0 }
|
22
|
+
#
|
23
|
+
# # more time-efficient batch insertion using arrays:
|
24
|
+
# ts = TimeSeries.new
|
25
|
+
# tt = [ 1.0, 1.2, 1.6, 1.9, 2.1 ]
|
26
|
+
# dd = [ {x: 2.0}, {x: 2.1}, {x: 2.5}, {x: 2.7}, {x: 2.85} ]
|
27
|
+
# ts.push_array(tt, dd)
|
28
|
+
#
|
29
|
+
# # retrieve closest element before given time
|
30
|
+
# ts[1.2] # => { x: 2.1 }
|
31
|
+
# ts[2.095] # => { x: 2.7 }
|
32
|
+
|
33
|
+
class TimeSeries
|
34
|
+
attr_reader :data, :processor
|
35
|
+
|
36
|
+
# Creates a TimeSeries object.
|
37
|
+
#
|
38
|
+
# @param arr_t [Array<Float>] Array of timestamps
|
39
|
+
# @param arr_v [Array] Array of corresponding values, should be of the same size as +arr_t+
|
40
|
+
#
|
41
|
+
# == Examples:
|
42
|
+
#
|
43
|
+
# ts = TimeSeries.new # creates empty TimeSeries object
|
44
|
+
#
|
45
|
+
# ts = TimeSeries.new([0.1, 0.5, 1.0], [120.0, 130.0, 140.0])
|
46
|
+
#
|
47
|
+
def initialize(arr_t = nil, arr_v = nil)
|
48
|
+
@data = []
|
49
|
+
@processor = nil
|
50
|
+
push_array(arr_t, arr_v) if arr_t && arr_v
|
51
|
+
end
|
52
|
+
|
53
|
+
# @return number of values in the series
|
54
|
+
#
|
55
|
+
def size
|
56
|
+
@data.size
|
57
|
+
end
|
58
|
+
|
59
|
+
# @return [Array] Array of timestamps
|
60
|
+
#
|
61
|
+
def keys
|
62
|
+
@data.map { |d| d[0] }
|
63
|
+
end
|
64
|
+
|
65
|
+
# @return [Array] Array of values
|
66
|
+
#
|
67
|
+
def values
|
68
|
+
@data.map { |d| d[1] }
|
69
|
+
end
|
70
|
+
|
71
|
+
# @return [Array, nil] First element of the time series
|
72
|
+
#
|
73
|
+
def first
|
74
|
+
@data.first
|
75
|
+
end
|
76
|
+
|
77
|
+
# @return [Float, nil] Timestamp of the first element
|
78
|
+
#
|
79
|
+
def t_first
|
80
|
+
first && first[0]
|
81
|
+
end
|
82
|
+
|
83
|
+
# @return [Array, nil] Last element of the time series
|
84
|
+
#
|
85
|
+
def last
|
86
|
+
@data.last
|
87
|
+
end
|
88
|
+
|
89
|
+
# @return [Float, nil] Timestamp of the last element
|
90
|
+
#
|
91
|
+
def t_last
|
92
|
+
last && last[0]
|
93
|
+
end
|
94
|
+
|
95
|
+
# Inserts new element into time series.
|
96
|
+
# @param t [Float] Timestamp of the new value
|
97
|
+
# @param v [Fixnum, Float, Array, Hash] New value
|
98
|
+
#
|
99
|
+
def push(t, v)
|
100
|
+
t = t.to_f
|
101
|
+
i = left_index_at(t)
|
102
|
+
if i.nil?
|
103
|
+
@data.unshift([t, v])
|
104
|
+
else
|
105
|
+
@data.insert(i + 1, [t, v])
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Alias for #push(t, v)
|
110
|
+
#
|
111
|
+
# == Example:
|
112
|
+
#
|
113
|
+
# ts = TimeSeries.new
|
114
|
+
# ts[1.0] = { x: 123.0 }
|
115
|
+
# ts[2.0] = { x: 125.0 }
|
116
|
+
#
|
117
|
+
# ts[1.0] # => { x: 123.0 }
|
118
|
+
#
|
119
|
+
# @param (see #push)
|
120
|
+
#
|
121
|
+
def []=(t, v)
|
122
|
+
push(t, v)
|
123
|
+
end
|
124
|
+
|
125
|
+
# Inserts batch of new values into time series. This method is more time efficient
|
126
|
+
# than using #push to insert elements one by one.
|
127
|
+
#
|
128
|
+
# @param arr_t [Array] Array of timestamps
|
129
|
+
# @param arr_v [Array] Array of corresponding values, should be of the same size as +arr_t+
|
130
|
+
#
|
131
|
+
def push_array(arr_t, arr_v)
|
132
|
+
arr_data = arr_t.zip(arr_v)
|
133
|
+
@data.concat(arr_data)
|
134
|
+
@data.sort_by! { |d| d[0] }
|
135
|
+
end
|
136
|
+
|
137
|
+
# @return index of the element preceding +t+
|
138
|
+
#
|
139
|
+
def left_index_at(t)
|
140
|
+
indices_at(t).first
|
141
|
+
end
|
142
|
+
|
143
|
+
# Returns value calculated at +t+.
|
144
|
+
#
|
145
|
+
# The actual value returned depends on the +processor+
|
146
|
+
# used by TimeSeries object. When no +processor+ is used, the returned value is the value
|
147
|
+
# of the last element preceding, or exactly at +t+.
|
148
|
+
#
|
149
|
+
def [](t)
|
150
|
+
i = left_index_at(t)
|
151
|
+
i && @data[i][1]
|
152
|
+
end
|
153
|
+
|
154
|
+
# @return [Array] indices of the elements surrounding +t+.
|
155
|
+
#
|
156
|
+
def indices_at(t)
|
157
|
+
bsearch_indices_at(t)
|
158
|
+
end
|
159
|
+
|
160
|
+
# Use +processor+
|
161
|
+
#
|
162
|
+
def use(processor_module)
|
163
|
+
@processor = processor_module
|
164
|
+
extend processor_module
|
165
|
+
end
|
166
|
+
end # class TimeSeries
|
167
|
+
end # module TimeSeriesMath
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'ElemwiseOperators' do
|
4
|
+
class A
|
5
|
+
end
|
6
|
+
|
7
|
+
subject { A.new.extend(ElemwiseOperators) }
|
8
|
+
|
9
|
+
describe '#elemwise_add' do
|
10
|
+
it 'should add Fixnum' do
|
11
|
+
expect(subject.elemwise_add( 1.0, 2.0 )).to eql 3.0
|
12
|
+
end
|
13
|
+
it 'should add Array(s)' do
|
14
|
+
expect(subject.elemwise_add([1.0], [2.0])).to eql [3.0]
|
15
|
+
end
|
16
|
+
it 'should add Hash(es)' do
|
17
|
+
expect(subject.elemwise_add({ x: 1.0 }, { x: 2.0 })).to eql({ x: 3.0 })
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe '#elemwise_sub' do
|
22
|
+
it 'should substract Fixnum' do
|
23
|
+
expect(subject.elemwise_sub( 1.0, 2.0 )).to eql -1.0
|
24
|
+
end
|
25
|
+
it 'should substract Array(s)' do
|
26
|
+
expect(subject.elemwise_sub([1.0], [2.0])).to eql [-1.0]
|
27
|
+
end
|
28
|
+
it 'should substract Hash(es)' do
|
29
|
+
expect(subject.elemwise_sub({ x: 1.0 }, { x: 2.0 })).to eql({ x: -1.0 })
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe '#elemwise_mul_scalar' do
|
34
|
+
it 'should multiply Fixnum' do
|
35
|
+
expect(subject.elemwise_mul_scalar( 2.0, 3.0 )).to eql 6.0
|
36
|
+
end
|
37
|
+
it 'should multiply Array(s)' do
|
38
|
+
expect(subject.elemwise_mul_scalar(2.0, [3.0])).to eql [6.0]
|
39
|
+
end
|
40
|
+
it 'should mutiply Hash(es)' do
|
41
|
+
expect(subject.elemwise_mul_scalar(2.0, { x: 3.0 })).to eql({ x: 6.0 })
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe LinearInterpolation do
|
4
|
+
|
5
|
+
let(:arr_t) { [1.0, 2.0, 3.0] }
|
6
|
+
let(:arr_v) { [100, 200, 300] }
|
7
|
+
let(:arr_v_a) { [[100], [200], [300]] }
|
8
|
+
let(:arr_v_h) { [{ x: 100 }, { x: 200 }, { x: 300 }] }
|
9
|
+
it { expect { TimeSeries.new.use(LinearInterpolation) }.to_not raise_error }
|
10
|
+
|
11
|
+
context 'when values are floats' do
|
12
|
+
subject { TimeSeries.new(arr_t, arr_v).use(LinearInterpolation) }
|
13
|
+
it { expect(subject[1.5]).to eql 150.to_f }
|
14
|
+
end
|
15
|
+
|
16
|
+
context 'when values are arrays' do
|
17
|
+
subject { TimeSeries.new(arr_t, arr_v_a).use(LinearInterpolation) }
|
18
|
+
it { expect(subject[1.5]).to eql [150.to_f] }
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'when values are hashes' do
|
22
|
+
subject { TimeSeries.new(arr_t, arr_v_h).use(LinearInterpolation) }
|
23
|
+
it { expect(subject[1.5]).to eql({ x: 150.to_f }) }
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'when requested value is out of range' do
|
27
|
+
subject { TimeSeries.new(arr_t, arr_v).use(LinearInterpolation) }
|
28
|
+
it { expect(subject[-1.0].to_f).to eql 100.to_f }
|
29
|
+
it { expect(subject[10.0].to_f).to eql 300.to_f }
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe TimeSeries do
|
4
|
+
it { should respond_to(:bsearch_indices_at) }
|
5
|
+
it { expect { subject.bsearch_indices_at(1.0) }.not_to raise_error }
|
6
|
+
it { expect(subject.bsearch_indices_at(1.0)).to eql [nil, nil] }
|
7
|
+
|
8
|
+
let(:arr_t) { [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0] }
|
9
|
+
let(:arr_t1) { [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] }
|
10
|
+
let(:arr_t2) { [1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 2.0] }
|
11
|
+
let(:arr_v) { [111, 222, 333, 444, 555, 666, 777] }
|
12
|
+
|
13
|
+
context 'when initialized with arrays' do
|
14
|
+
subject { TimeSeries.new(arr_t, arr_v) }
|
15
|
+
|
16
|
+
describe '#bsearch_indices_at' do
|
17
|
+
it 'should return [nil, 0] if t < first element' do
|
18
|
+
expect(subject.bsearch_indices_at(-1.0)).to eql [nil, 0]
|
19
|
+
end
|
20
|
+
it 'should return [i_k, i_k+1] if T(i_k) <= t < T(i_k+1)' do
|
21
|
+
expect(subject.bsearch_indices_at(1.0)).to eql [0, 1]
|
22
|
+
expect(subject.bsearch_indices_at(1.5)).to eql [0, 1]
|
23
|
+
end
|
24
|
+
it 'should return last element index [N, nil] if T(N) <= t' do
|
25
|
+
expect(subject.bsearch_indices_at(10.0)).to eql [6, nil]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'when initialized with several items with same timestamp' do
|
31
|
+
let(:ts1) { TimeSeries.new(arr_t1, arr_v) }
|
32
|
+
let(:ts2) { TimeSeries.new(arr_t2, arr_v) }
|
33
|
+
|
34
|
+
describe '#bsearch_indices_at' do
|
35
|
+
it 'should return last element index pair [N, nil] if T(N) <= t' do
|
36
|
+
expect(ts1.bsearch_indices_at(1.0)).to eql [6, nil]
|
37
|
+
expect(ts1.bsearch_indices_at(10.0)).to eql [6, nil]
|
38
|
+
end
|
39
|
+
it 'should return index pair of last element in serie of equal timestamps' do
|
40
|
+
expect(ts2.bsearch_indices_at(1.0)).to eql [2, 3]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe TimeSeries do
|
4
|
+
context 'when empty' do
|
5
|
+
subject { TimeSeries.new }
|
6
|
+
it { expect(subject.data).to eql [] }
|
7
|
+
it { expect(subject.first).to be nil }
|
8
|
+
it { expect(subject.last).to be nil }
|
9
|
+
it { expect(subject.t_first).to be nil }
|
10
|
+
it { expect(subject.t_last).to be nil }
|
11
|
+
it { expect(subject.size).to eql 0 }
|
12
|
+
it { expect(subject.keys).to eql [] }
|
13
|
+
it { expect(subject.values).to eql [] }
|
14
|
+
it { expect(subject.left_index_at(1.0)).to be nil }
|
15
|
+
it { expect(subject.indices_at(1.0)).to eql [nil, nil] }
|
16
|
+
it { expect(subject[1.0]).to be nil }
|
17
|
+
end
|
18
|
+
|
19
|
+
let(:arr_t) { [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0] }
|
20
|
+
let(:arr_t1) { [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] }
|
21
|
+
let(:arr_t2) { [1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 2.0] }
|
22
|
+
let(:arr_v) { [111, 222, 333, 444, 555, 666, 777] }
|
23
|
+
it { expect { TimeSeries.new(arr_t, arr_v) }.to_not raise_error }
|
24
|
+
|
25
|
+
context 'when initialized with arrays' do
|
26
|
+
subject { TimeSeries.new(arr_t, arr_v) }
|
27
|
+
it { expect(subject.size).to eql 7 }
|
28
|
+
it { expect(subject.first).to eql [1.0, 111] }
|
29
|
+
it { expect(subject.last).to eql [7.0, 777] }
|
30
|
+
it { expect(subject.t_first).to eql 1.0 }
|
31
|
+
it { expect(subject.t_last).to eql 7.0 }
|
32
|
+
|
33
|
+
describe '#left_index_at' do
|
34
|
+
it 'should return nil if t < first element' do
|
35
|
+
expect(subject.left_index_at(-1.0)).to be nil
|
36
|
+
end
|
37
|
+
it 'should return i_k if T(i_k) <= t < T(i_k+1)' do
|
38
|
+
expect(subject.left_index_at(1.0)).to be 0
|
39
|
+
expect(subject.left_index_at(1.5)).to be 0
|
40
|
+
end
|
41
|
+
it 'should return last element index (N) if T(N) <= t' do
|
42
|
+
expect(subject.left_index_at(10.0)).to be 6
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe '#[]' do
|
47
|
+
it 'should return nil if t < first element' do
|
48
|
+
expect(subject[-1.0]).to be nil
|
49
|
+
end
|
50
|
+
it 'should return i_k if T(i_k) <= t < T(i_k+1)' do
|
51
|
+
expect(subject[1.0]).to be 111
|
52
|
+
expect(subject[1.5]).to be 111
|
53
|
+
end
|
54
|
+
it 'should return last element index (N) if T(N) <= t' do
|
55
|
+
expect(subject[10.0]).to be 777
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context 'when initialized with several items with same timestamp' do
|
61
|
+
let(:ts1) { TimeSeries.new(arr_t1, arr_v) }
|
62
|
+
let(:ts2) { TimeSeries.new(arr_t2, arr_v) }
|
63
|
+
|
64
|
+
describe '#left_index_at' do
|
65
|
+
it 'should return last element index (N) if T(N) <= t' do
|
66
|
+
expect(ts1.left_index_at(1.0)).to be 6
|
67
|
+
expect(ts1.left_index_at(10.0)).to be 6
|
68
|
+
end
|
69
|
+
it 'should return index of last element in serie of equal timestamps' do
|
70
|
+
expect(ts2.left_index_at(1.0)).to be 2
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe '#indices_at' do
|
75
|
+
it 'should return last element index pair [N, nil] if T(N) <= t' do
|
76
|
+
expect(ts1.indices_at(1.0)).to eql [6, nil]
|
77
|
+
expect(ts1.indices_at(10.0)).to eql [6, nil]
|
78
|
+
end
|
79
|
+
it 'should return index pair of last element in serie of equal timestamps' do
|
80
|
+
expect(ts2.indices_at(1.0)).to eql [2, 3]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
context 'when adding new items' do
|
86
|
+
subject { TimeSeries.new(arr_t, arr_v) }
|
87
|
+
it 'should place new items at correct place' do
|
88
|
+
expect { subject.push(1.5, 123) }.to_not raise_error
|
89
|
+
expect(subject.keys).to eql [1.0, 1.5, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]
|
90
|
+
end
|
91
|
+
it 'should allow []= syntax' do
|
92
|
+
expect { subject[1.5] = 123 }.to_not raise_error
|
93
|
+
expect(subject.keys).to eql [1.0, 1.5, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe TimeSeries do
|
4
|
+
module TimeSeriesTestProcessor
|
5
|
+
end
|
6
|
+
|
7
|
+
it { should respond_to(:use) }
|
8
|
+
it { should respond_to(:processor) }
|
9
|
+
it { expect(subject.processor).to be nil }
|
10
|
+
it { expect(subject.use(TimeSeriesTestProcessor)).to eql subject }
|
11
|
+
|
12
|
+
context 'when using a processor' do
|
13
|
+
subject { TimeSeries.new.use(TimeSeriesTestProcessor) }
|
14
|
+
it { expect(subject.processor).to eql TimeSeriesTestProcessor }
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'time_series_math/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "time_series_math"
|
8
|
+
spec.version = TimeSeriesMath::VERSION
|
9
|
+
spec.authors = ["Alex Kukushkin"]
|
10
|
+
spec.email = ["alex@kukushk.in"]
|
11
|
+
spec.summary = %q{Simple time series math}
|
12
|
+
spec.description = %q{Time series math support}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib", "ext"]
|
20
|
+
|
21
|
+
spec.extensions = Dir['ext/**/extconf.rb']
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
24
|
+
spec.add_development_dependency "rake"
|
25
|
+
spec.add_development_dependency "rspec"
|
26
|
+
spec.add_development_dependency "rake-compiler"
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: time_series_math
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alex Kukushkin
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-05-31 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.6'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.6'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake-compiler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: Time series math support
|
70
|
+
email:
|
71
|
+
- alex@kukushk.in
|
72
|
+
executables: []
|
73
|
+
extensions:
|
74
|
+
- ext/time_series_math_c/extconf.rb
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- ".gitignore"
|
78
|
+
- Gemfile
|
79
|
+
- LICENSE.txt
|
80
|
+
- README.md
|
81
|
+
- Rakefile
|
82
|
+
- examples/ts_benchmark.rb
|
83
|
+
- ext/time_series_math_c/extconf.rb
|
84
|
+
- ext/time_series_math_c/time_series_math_c.c
|
85
|
+
- lib/time_series_math.rb
|
86
|
+
- lib/time_series_math/elemwise_operators.rb
|
87
|
+
- lib/time_series_math/linear_interpolation.rb
|
88
|
+
- lib/time_series_math/time_series.rb
|
89
|
+
- lib/time_series_math/version.rb
|
90
|
+
- spec/spec_helper.rb
|
91
|
+
- spec/time_series_math/elemwise_operators_spec.rb
|
92
|
+
- spec/time_series_math/linear_interpolation_spec.rb
|
93
|
+
- spec/time_series_math/time_series_math_c_spec.rb
|
94
|
+
- spec/time_series_math/time_series_spec.rb
|
95
|
+
- spec/time_series_math/time_series_use_api_spec.rb
|
96
|
+
- spec/time_series_math_spec.rb
|
97
|
+
- time_series_math.gemspec
|
98
|
+
homepage: ''
|
99
|
+
licenses:
|
100
|
+
- MIT
|
101
|
+
metadata: {}
|
102
|
+
post_install_message:
|
103
|
+
rdoc_options: []
|
104
|
+
require_paths:
|
105
|
+
- lib
|
106
|
+
- ext
|
107
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - ">="
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '0'
|
112
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
requirements: []
|
118
|
+
rubyforge_project:
|
119
|
+
rubygems_version: 2.2.2
|
120
|
+
signing_key:
|
121
|
+
specification_version: 4
|
122
|
+
summary: Simple time series math
|
123
|
+
test_files:
|
124
|
+
- spec/spec_helper.rb
|
125
|
+
- spec/time_series_math/elemwise_operators_spec.rb
|
126
|
+
- spec/time_series_math/linear_interpolation_spec.rb
|
127
|
+
- spec/time_series_math/time_series_math_c_spec.rb
|
128
|
+
- spec/time_series_math/time_series_spec.rb
|
129
|
+
- spec/time_series_math/time_series_use_api_spec.rb
|
130
|
+
- spec/time_series_math_spec.rb
|
131
|
+
has_rdoc:
|