time_series_math 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|