ts 1.0.0
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 +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: []
|