ts 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +0 -0
- data/README.md +32 -0
- data/Rakefile +8 -0
- data/lib/ts.rb +182 -0
- data/test/test_ts.rb +60 -0
- data/ts.gemspec +20 -0
- metadata +48 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 28be043c878e154e056aca129a35fb9a2c225e1d
|
4
|
+
data.tar.gz: 9f6582a2e8298dcabfe7103daeedb37eec3fd2ee
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 79d5dbdade0dd0c12f9213a34f6f4f36dbe84252874d04e9189da4bb6c9e74bfe25e6ccf7e95b3b13304426518e3da815b9c683afc3c45e904a4e4b1329bb2f3
|
7
|
+
data.tar.gz: e285a9dcc3389ff35de9891cd4dbef0af18d72a05f1ebe5a6f6ae179547f741007be9518a46c74fc05760bba0fe91af5298c5a96cd41f915d30e26ec51dc7212
|
data/.gitignore
ADDED
File without changes
|
data/README.md
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
### ts.rb
|
2
|
+
|
3
|
+
Utility class for time series data, which does not require periodicity.
|
4
|
+
|
5
|
+
##### Install
|
6
|
+
|
7
|
+
```
|
8
|
+
gem install ts
|
9
|
+
```
|
10
|
+
|
11
|
+
##### Usage
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
require "ts"
|
15
|
+
|
16
|
+
ts = TS.new([
|
17
|
+
[time, value],
|
18
|
+
# ...
|
19
|
+
[time, value]
|
20
|
+
])
|
21
|
+
|
22
|
+
ts.each { |time, value|
|
23
|
+
#...
|
24
|
+
}
|
25
|
+
|
26
|
+
ts.stats
|
27
|
+
ts.slice start, finish
|
28
|
+
ts.after time
|
29
|
+
ts.before time
|
30
|
+
```
|
31
|
+
|
32
|
+
See rdoc for all methods.
|
data/Rakefile
ADDED
data/lib/ts.rb
ADDED
@@ -0,0 +1,182 @@
|
|
1
|
+
#
|
2
|
+
# TS
|
3
|
+
#
|
4
|
+
# Utility class for [timestamp, number] tuples, where periodicity is not
|
5
|
+
# guaranteed.
|
6
|
+
#
|
7
|
+
class TS
|
8
|
+
|
9
|
+
Version = "1.0.0"
|
10
|
+
|
11
|
+
include Enumerable
|
12
|
+
|
13
|
+
attr_reader :data
|
14
|
+
|
15
|
+
# +data+ an array of [timestamp/time, number] tuples
|
16
|
+
def initialize data
|
17
|
+
if data.nil?
|
18
|
+
raise "Cannot instantiate timeseries without data"
|
19
|
+
end
|
20
|
+
|
21
|
+
@data = data
|
22
|
+
end
|
23
|
+
|
24
|
+
# The number of elements in the set
|
25
|
+
def size
|
26
|
+
@data.size
|
27
|
+
end
|
28
|
+
|
29
|
+
# see Enumerable
|
30
|
+
def each
|
31
|
+
@data.each { |v| yield *v }
|
32
|
+
end
|
33
|
+
|
34
|
+
# map the [time,value] tuples into other [time,value] tuples
|
35
|
+
def map
|
36
|
+
TS.new(@data.map { |v| yield *v })
|
37
|
+
end
|
38
|
+
|
39
|
+
def stats
|
40
|
+
return @stats if @stats
|
41
|
+
|
42
|
+
min = Float::MAX
|
43
|
+
max = Float::MIN
|
44
|
+
sum = 0.0
|
45
|
+
sum2 = 0.0
|
46
|
+
|
47
|
+
each { |time, val|
|
48
|
+
min = val if val < min
|
49
|
+
max = val if val > max
|
50
|
+
sum = sum + val
|
51
|
+
sum2 = sum2 + val ** 2
|
52
|
+
}
|
53
|
+
|
54
|
+
@stats = {
|
55
|
+
:num => size,
|
56
|
+
:min => min,
|
57
|
+
:max => max,
|
58
|
+
:sum => sum,
|
59
|
+
:mean => sum / size,
|
60
|
+
:stddev => Math.sqrt((sum2 / size) - ((sum / size) ** 2))
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
# slice a timeseries by timestamps
|
65
|
+
# +t1+ start time
|
66
|
+
# +t2+ end time
|
67
|
+
def slice t1, t2
|
68
|
+
idx1 = nearest(t1)
|
69
|
+
idx2 = nearest(t2)
|
70
|
+
|
71
|
+
# don't include a value not in range
|
72
|
+
if time_at(idx1) < t1
|
73
|
+
idx1 += 1
|
74
|
+
end
|
75
|
+
|
76
|
+
# slice goes up to, but doesn't include, so only
|
77
|
+
# add if the nearest is less than
|
78
|
+
if time_at(idx2) < t2
|
79
|
+
idx2 += 1
|
80
|
+
end
|
81
|
+
|
82
|
+
TS.new(@data[idx1..idx2])
|
83
|
+
end
|
84
|
+
|
85
|
+
# give the timeseries with values after time
|
86
|
+
# +time+ the time boundary
|
87
|
+
def after time
|
88
|
+
idx = nearest(time)
|
89
|
+
if time_at(idx) <= time
|
90
|
+
idx += 1
|
91
|
+
end
|
92
|
+
|
93
|
+
TS.new(@data[idx..-1])
|
94
|
+
end
|
95
|
+
|
96
|
+
# give the timeseries with values before time
|
97
|
+
# +time+ the time boundary
|
98
|
+
def before time
|
99
|
+
idx = nearest(time)
|
100
|
+
if time_at(idx) < time
|
101
|
+
idx += 1
|
102
|
+
end
|
103
|
+
|
104
|
+
TS.new(@data[0..idx-1])
|
105
|
+
end
|
106
|
+
|
107
|
+
def value_at idx
|
108
|
+
@data[idx].last
|
109
|
+
end
|
110
|
+
|
111
|
+
def time_at idx
|
112
|
+
@data[idx].first
|
113
|
+
end
|
114
|
+
|
115
|
+
# find the nearest idx for a given time
|
116
|
+
# using a fuzzy binary search
|
117
|
+
def nearest time
|
118
|
+
bsearch time, 0, size - 1
|
119
|
+
end
|
120
|
+
|
121
|
+
def timestamps
|
122
|
+
@data.transpose.first
|
123
|
+
end
|
124
|
+
|
125
|
+
def values
|
126
|
+
@data.transpose.last
|
127
|
+
end
|
128
|
+
|
129
|
+
# Run a regression and calculate r, r2, the slope, and intercept
|
130
|
+
def regression
|
131
|
+
return @regression if @regression
|
132
|
+
|
133
|
+
times, values = @data.transpose
|
134
|
+
|
135
|
+
t_mean = times.reduce(:+) / size
|
136
|
+
v_mean = values.reduce(:+) / size
|
137
|
+
|
138
|
+
slope = (0..size - 1).inject(0) { |sum, n|
|
139
|
+
sum + (times[n] - t_mean) * (values[n] - v_mean)
|
140
|
+
} / times.inject { |sum, n|
|
141
|
+
sum + (n - t_mean) ** 2
|
142
|
+
}
|
143
|
+
|
144
|
+
# now r2
|
145
|
+
r = slope * (stddev(times) / stddev(values))
|
146
|
+
|
147
|
+
@regression = {
|
148
|
+
:r2 => r ** 2,
|
149
|
+
:slope => slope,
|
150
|
+
:y_intercept => v_mean - (slope * t_mean)
|
151
|
+
}
|
152
|
+
end
|
153
|
+
|
154
|
+
private
|
155
|
+
|
156
|
+
# Find the nearest index for a given time (fuzzy search)
|
157
|
+
def bsearch time, idx1, idx2
|
158
|
+
mid = ((idx2 - idx1) / 2.0).floor.to_i + idx1
|
159
|
+
if idx1 == mid
|
160
|
+
diff1 = (time_at(idx1) - time).abs
|
161
|
+
diff2 = (time_at(idx2) - time).abs
|
162
|
+
diff2 > diff1 ? idx1 : idx2
|
163
|
+
elsif time < time_at(mid)
|
164
|
+
bsearch time, idx1, mid
|
165
|
+
elsif time > time_at(mid)
|
166
|
+
bsearch time, mid, idx2
|
167
|
+
else
|
168
|
+
mid
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def stddev data
|
173
|
+
sum = 0.0
|
174
|
+
sum2 = 0.0
|
175
|
+
data.each { |v|
|
176
|
+
sum += v
|
177
|
+
sum2 += v ** 2
|
178
|
+
}
|
179
|
+
Math.sqrt(sum2 / data.size - (sum / data.size) ** 2)
|
180
|
+
end
|
181
|
+
|
182
|
+
end
|
data/test/test_ts.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require "test/unit"
|
2
|
+
require "ts"
|
3
|
+
|
4
|
+
class TSTest < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def setup
|
7
|
+
raw = (1..1000).map { |i|
|
8
|
+
[i * 1000, i.to_f]
|
9
|
+
}
|
10
|
+
@ts = TS.new(raw)
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_init
|
14
|
+
assert_equal 1000, @ts.size
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_enum
|
18
|
+
assert_equal 1000, @ts.count
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_stats
|
22
|
+
assert_equal 1, @ts.stats[:min]
|
23
|
+
assert_equal 1000, @ts.stats[:max]
|
24
|
+
assert_equal 1000, @ts.stats[:num]
|
25
|
+
assert_equal (1000 * (1000 + 1)) / 2, @ts.stats[:sum]
|
26
|
+
assert_in_delta 288, @ts.stats[:stddev], 1.0
|
27
|
+
assert_in_delta 500, @ts.stats[:mean], 1.0
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_slice
|
31
|
+
assert_equal 3, @ts.slice(1000, 3000).size
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_after
|
35
|
+
assert_equal 999, @ts.after(1000).size
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_before
|
39
|
+
assert_equal 1, @ts.before(2000).size
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_timestamps
|
43
|
+
assert_equal 1000, @ts.timestamps.size
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_values
|
47
|
+
assert_equal 1000, @ts.values.last
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_regression
|
51
|
+
assert_in_delta 0.001, @ts.regression[:slope], 0.001
|
52
|
+
assert_in_delta 1.0, @ts.regression[:r2], 0.01
|
53
|
+
assert_in_delta -1.5, @ts.regression[:y_intercept], 0.1
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_collect
|
57
|
+
assert @ts.map { |t, v| [t, v * 2] }.stats[:min] == 2
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
data/ts.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
$:.push File.expand_path("../lib", __FILE__)
|
2
|
+
require "ts"
|
3
|
+
|
4
|
+
spec = Gem::Specification.new do |s|
|
5
|
+
s.name = "ts"
|
6
|
+
s.version = TS::Version
|
7
|
+
s.date = "2013-08-14"
|
8
|
+
s.summary = "Utility gem for numeric time series data"
|
9
|
+
s.email = "dan.simpson@gmail.com"
|
10
|
+
s.homepage = "https://github.com/dansimpson/ts.rb"
|
11
|
+
s.description = "Utilities for numeric time series data"
|
12
|
+
s.has_rdoc = true
|
13
|
+
|
14
|
+
s.authors = ["Dan Simpson"]
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
|
20
|
+
end
|
metadata
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ts
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dan Simpson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-08-14 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Utilities for numeric time series data
|
14
|
+
email: dan.simpson@gmail.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- .gitignore
|
20
|
+
- README.md
|
21
|
+
- Rakefile
|
22
|
+
- lib/ts.rb
|
23
|
+
- test/test_ts.rb
|
24
|
+
- ts.gemspec
|
25
|
+
homepage: https://github.com/dansimpson/ts.rb
|
26
|
+
licenses: []
|
27
|
+
metadata: {}
|
28
|
+
post_install_message:
|
29
|
+
rdoc_options: []
|
30
|
+
require_paths:
|
31
|
+
- lib
|
32
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
33
|
+
requirements:
|
34
|
+
- - '>='
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '0'
|
37
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - '>='
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
requirements: []
|
43
|
+
rubyforge_project:
|
44
|
+
rubygems_version: 2.0.3
|
45
|
+
signing_key:
|
46
|
+
specification_version: 4
|
47
|
+
summary: Utility gem for numeric time series data
|
48
|
+
test_files: []
|