class TS

TS

Utility class for [timestamp, number] tuples, where periodicity is not guaranteed.

Constants

Version

Attributes

data[R]

Public Class Methods

new(data) click to toggle source

data an array of [timestamp/time, number] tuples

# File lib/ts.rb, line 16
def initialize data
  if data.nil?
    raise "Cannot instantiate timeseries without data"
  end

  @data = data
end

Public Instance Methods

after(time) click to toggle source

give the timeseries with values after time time the time boundary

# File lib/ts.rb, line 124
def after time
  idx = nearest(time)
  if time_at(idx) <= time
    idx += 1
  end

  TS.new(@data[idx..-1])
end
before(time) click to toggle source

give the timeseries with values before time time the time boundary

# File lib/ts.rb, line 135
def before time
  idx = nearest(time)
  return TS.new([]) if idx <= 0
  if time_at(idx) < time
    idx += 1
  end
  TS.new(@data[0..idx-1])
end
each() { |*v| ... } click to toggle source

see Enumerable

# File lib/ts.rb, line 30
def each
  @data.each { |v| yield *v }
end
map() { |*v| ... } click to toggle source

map the [time,value] tuples into other [time,value] tuples

# File lib/ts.rb, line 35
def map
  TS.new(@data.map { |v| yield *v })
end
mean() click to toggle source

get the mean value

# File lib/ts.rb, line 92
def mean
  stats[:mean]
end
nearest(time) click to toggle source

find the nearest idx for a given time using a fuzzy binary search

# File lib/ts.rb, line 158
def nearest time
  bsearch time, 0, size - 1
end
projected_time(value) click to toggle source

Estimate the time for a given value. Assumes a fairly linear model.

x = (y - b) / m

value the timestamp of the value you wish to predict

# File lib/ts.rb, line 218
def projected_time value
  (value - regression[:y_intercept]) / regression[:slope]
end
projected_value(time) click to toggle source

Project the value at a given time using the regresion

y = mx + b

time the timestamp of the value you wish to predict

# File lib/ts.rb, line 208
def projected_value time
  regression[:slope] * time + regression[:y_intercept]
end
regression() click to toggle source

Run a regression on the series. Useful for weak projections and testing if your project is accurate (r2 =~ 1)

returns {

:r2 => ...,
:slope => ...,
:y_intercept => ...

}

# File lib/ts.rb, line 180
def regression
  return @regression if @regression

  times, values = @data.transpose

  t_mean = times.reduce(:+) / size
  v_mean = values.reduce(:+) / size

  slope = (0..size - 1).inject(0.0) { |sum, n|
    sum + (times[n] - t_mean) * (values[n] - v_mean)
  } / times.inject(0.0) { |sum, n|
    sum + (n - t_mean) ** 2
  }

  r = slope * (_stddev(times) / _stddev(values))

  @regression = {
    :r2 => r * r,
    :slope => slope,
    :y_intercept => v_mean - (slope * t_mean)
  }
end
size() click to toggle source

The number of elements in the set

# File lib/ts.rb, line 25
def size
  @data.size
end
slice(t1, t2) click to toggle source

slice a timeseries by timestamps t1 start time t2 end time

# File lib/ts.rb, line 104
def slice t1, t2
  idx1 = nearest(t1)
  idx2 = nearest(t2)

  # don't include a value not in range
  if time_at(idx1) < t1
    idx1 += 1
  end

  # slice goes up to, but doesn't include, so only
  # add if the nearest is less than
  if time_at(idx2) < t2
    idx2 += 1
  end

  TS.new(@data[idx1..idx2])
end
sma(size) click to toggle source

run a simple moving average, and return a new TS instance size the size of the window

# File lib/ts.rb, line 41
def sma size
  buf = []
  sum = 0

  map { |t, v|
    buf << v
    sum += v

    if buf.size > size
      sum -= buf.shift
    end

    [t, sum / buf.size]
  }
end
stats() click to toggle source

generate some statistics from the values of the series returns {

:num => ...,
:min => ...,
:max => ...,
:sum => ...,
:mean => ...,
:stddev => ...,

}

# File lib/ts.rb, line 66
def stats
  return @stats if @stats

  min  = Float::MAX
  max  = Float::MIN
  sum  = 0.0
  sum2 = 0.0

  each { |time, val|
    min = val if val < min
    max = val if val > max
    sum += val
    sum2 += val ** 2
  }

  @stats = {
    :num => size,
    :min => min,
    :max => max,
    :sum => sum,
    :mean => sum / size,
    :stddev => Math.sqrt((sum2 / size) - ((sum / size) ** 2))
  }
end
stddev() click to toggle source

get the standard deviation of the values

# File lib/ts.rb, line 97
def stddev
  stats[:stddev]
end
time_at(idx) click to toggle source

fetch the time at a given index idx the array index of the data

# File lib/ts.rb, line 152
def time_at idx
  @data[idx].first
end
timestamps() click to toggle source

get the timestamp series

# File lib/ts.rb, line 163
def timestamps
  @data.transpose.first
end
value_at(idx) click to toggle source

fetch the value at a given index idx the array index of the data

# File lib/ts.rb, line 146
def value_at idx
  @data[idx].last
end
values() click to toggle source

get the value series

# File lib/ts.rb, line 168
def values
  @data.transpose.last
end

Private Instance Methods

_stddev(data) click to toggle source

calculate the std deviation of the 1d data set

# File lib/ts.rb, line 241
def _stddev data
  sum  = 0.0
  sum2 = 0.0
  data.each { |v|
    sum  += v
    sum2 += v ** 2
  }
  Math.sqrt((sum2 / data.size) - ((sum / data.size) ** 2))
end
bsearch(time, idx1, idx2) click to toggle source

Find the nearest index for a given time (fuzzy search)

# File lib/ts.rb, line 225
def bsearch time, idx1, idx2
  mid = ((idx2 - idx1) / 2.0).floor.to_i + idx1
  if idx1 == mid
    diff1 = (time_at(idx1) - time).abs
    diff2 = (time_at(idx2) - time).abs
    diff2 > diff1 ? idx1 : idx2
  elsif time < time_at(mid)
    bsearch time, idx1, mid
  elsif time > time_at(mid)
    bsearch time, mid, idx2
  else
    mid
  end
end