savgol 0.2.1 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 74cb6ba4eb41d56918d8e0994ecab490234bae85
4
- data.tar.gz: 60e5ce50c918ac33ccf028a9e26773d06d59e7af
3
+ metadata.gz: bc7a6227404449143b6b264e85308bda89e5e2fa
4
+ data.tar.gz: 5e4089173d7d618e2bfe372a5758e78c6be22f36
5
5
  SHA512:
6
- metadata.gz: 9698cb0c54a48b474f27badfa100b65ca92857829f59ed38ed304c5e9ed4428e28dbf260c6829731c5dbef197279d4796212d0ce9ece1252cbaa809a1fb0730e
7
- data.tar.gz: cabf344682dbe30bd2d1beb1209dbdbf7530fcadfd70079fcd5edb967fcbd41bce049235c7d2f9482e10d007bd435f3afe2f387b5390163940a97cfe748c8850
6
+ metadata.gz: 6552c4e22998e3f8fb8b594f89684c42fe296496e1481b7f03aef6d43d68032ba23a8341a8288e2695b73b7e5527fc382f92dba9a02ff84764395949735c1836
7
+ data.tar.gz: dab124a5685cd5c3215d9b3ab628d626ec44590e883c1d340d4bbcad278d7a74f426520abdd19a8cbc16ee2dddd652014ecc1001a6ff7dcf9279c65ce91ce763
data/README.md CHANGED
@@ -2,17 +2,48 @@
2
2
 
3
3
  Provides implementations of Savitzky-Golay smoothing (filtering).
4
4
 
5
- The gem is based on the [scipy implementation](http://www.scipy.org/Cookbook/SavitzkyGolay). A good explanation of the process may be found [here on stackexchange](http://dsp.stackexchange.com/a/9494).
5
+ The gem is based on the [scipy implementation](http://www.scipy.org/Cookbook/SavitzkyGolay) (gives exactly the same result). A good explanation of the process may be found [here on stackexchange](http://dsp.stackexchange.com/a/9494).
6
6
 
7
7
  # Examples
8
8
 
9
- ## Array implementation
9
+ ## Evenly spaced data
10
+
11
+ ### Array implementation (object oriented)
10
12
 
11
13
  require 'savgol/array'
12
14
  data = [1, 2, 3, 4, 3.5, 5, 3, 2.2, 3, 0, -1, 2, 0, -2, -5, -8, -7, -2, 0, 1, 1]
13
15
  # window size = 5, polynomial order = 3
14
16
  data.savgol(5,3)
15
17
 
18
+ ### Module implementation
19
+
20
+ require 'savgol'
21
+ data = [1, 2, 3, 4, 3.5, 5, 3, 2.2, 3, 0, -1, 2, 0, -2, -5, -8, -7, -2, 0, 1, 1]
22
+ # window size = 5, polynomial order = 3
23
+ Savgol.savgol(data, 5,3)
24
+
25
+ ## Uneven data
26
+
27
+ The speed gain of the Savitzky-Golay filter is lost when interpolating
28
+ unevenly spaced data and devolves into simple polynomial linear regression [At
29
+ least I believe they are equivalent in complexity since both require a matrix
30
+ pseudo-inverse calculation as the most complex operation]. Even though it may
31
+ be much slower, a filter that handles unevenly spaced data may be useful in
32
+ some cases.
33
+
34
+ require 'savgol'
35
+ xvals = %w(-1 0 2 3 4 7 8 10 11 12 13 14 17 18).map &:to_f
36
+ yvals = %w(-2 1 0 1 1 3 4 7 8 9 7 4 1 2).map {|v| v.to_f + 30 }
37
+
38
+ ### Interpolate at the given x-values
39
+
40
+ Savgol.savgol_uneven(xvals, yvals, 5, 2)
41
+
42
+ ### Interpolate at a new set of x values
43
+
44
+ new_xvals = xvals.map {|v| v + 0.5 }
45
+ Savgol.savgol_uneven(xvals, yvals, 5, 2, new_xvals: new_xvals)
46
+
16
47
  # Installation
17
48
 
18
49
  gem install savgol
@@ -1,13 +1,176 @@
1
1
  require 'savgol/version'
2
- require 'savgol/array'
2
+ require 'matrix'
3
3
 
4
- module Savgol
4
+ class NilEnumerator < Enumerator
5
+ def initialize(enum)
6
+ @enum = enum
7
+ end
8
+
9
+ def next
10
+ begin
11
+ @enum.next
12
+ rescue StopIteration
13
+ nil
14
+ end
15
+ end
5
16
  end
17
+
18
+ module Savgol
19
+ class << self
20
+ # Does simple least squares to fit a polynomial based on the given x
21
+ # values. The major speed-boost of doing savgol is lost for uneven time points.
22
+ # TODO: implement for different derivatives; implement with a window that
23
+ # is of fixed size (not based on the number of points)
24
+ def savgol_uneven(xvals, yvals, window_points=11, order=4, check_args: false, new_xvals: nil)
25
+ sg_check_arguments(window_points, order) if check_args
26
+
27
+ half_window = (window_points -1) / 2
28
+ yvals_padded = sg_pad_ends(yvals, half_window)
29
+ xvals_padded = sg_pad_xvals(xvals, half_window)
30
+ xvals_size = xvals.size
31
+
32
+ if new_xvals
33
+ nearest_xval_indices = sg_nearest_index(xvals, new_xvals)
34
+ new_xvals.zip(nearest_xval_indices).map do |new_xval, index|
35
+ sg_regress_and_find(
36
+ xvals_padded[index,window_points],
37
+ yvals_padded[index,window_points],
38
+ order,
39
+ new_xval
40
+ )
41
+ end
42
+ else
43
+ xs_iter = xvals_padded.each_cons(window_points)
44
+ yvals_padded.each_cons(window_points).map do |ys|
45
+ xs = xs_iter.next
46
+ sg_regress_and_find(xs, ys, order, xs[half_window])
47
+ end
48
+ end
49
+ end
50
+
51
+ # returns the nearest index in original_vals for each value in new_vals
52
+ # (assumes both are sorted arrays). complexity: O(n + m)
53
+ def sg_nearest_index(original_vals, new_vals)
54
+ newval_iter = NilEnumerator.new(new_vals.each)
55
+ indices = []
56
+
57
+ index_iter = NilEnumerator.new((0...original_vals.size).each)
58
+ index = index_iter.next
59
+ newval=newval_iter.next
60
+ last_index = original_vals.size-1
61
+
62
+ until newval >= original_vals[index]
63
+ indices << index
64
+ break unless newval = newval_iter.next
65
+ end
66
+
67
+ loop do
68
+ break unless newval
69
+
70
+ if index.nil?
71
+ indices << last_index
72
+ else
73
+ until newval <= original_vals[index]
74
+ index = index_iter.next
75
+ if !index
76
+ indices << last_index
77
+ break
78
+ end
79
+ end
80
+
81
+ if index
82
+ if newval < original_vals[index]
83
+ indices << ((newval - original_vals[index-1]) <= (original_vals[index] - newval) ? index-1 : index)
84
+ else
85
+ indices << index
86
+ end
87
+ end
88
+ end
89
+ newval = newval_iter.next
90
+ end
91
+
92
+ indices
93
+ end
6
94
 
7
- # ar = [1, 2, 3, 4, 3.5, 5, 3, 2.2, 3, 0, -1, 2, 0, -2, -5, -8, -7, -2, 0, 1, 1]
8
- # numpy_savgol_output = [1.0, 2.0, 3.12857143, 3.57142857, 4.27142857, 4.12571429, 3.36857143, 2.69714286, 2.04, 0.32571429, -0.05714286, 0.8, 0.51428571, -2.17142857, -5.25714286, -7.65714286, -6.4, -2.77142857, 0.17142857, 0.91428571, 1.0]
9
- # sg = ar.savgol(5,3)
10
- #
11
- # sg.zip(numpy_savgol_output) do |sgv, numpy_sgv|
12
- # p "#{sgv} vs #{numpy_sgv} diff #{(sgv - numpy_sgv).abs.round(8)}"
13
- # end
95
+ def sg_regress_and_find(xdata, ydata, order, xval)
96
+ xdata_matrix = xdata.map { |xi| (0..order).map { |pow| (xi**pow).to_f } }
97
+ mx = Matrix[*xdata_matrix]
98
+ my = Matrix.column_vector(ydata)
99
+ ((mx.t * mx).inv * mx.t * my).transpose.to_a[0].
100
+ zip((0..order).to_a).
101
+ map {|coeff, pow| coeff * (xval**pow) }.
102
+ reduce(:+)
103
+ end
104
+
105
+ def savgol(array, window_points=11, order=4, deriv: 0, check_args: false)
106
+ sg_check_arguments(window_points, order) if check_args
107
+ half_window = (window_points -1) / 2
108
+ weights = sg_weights(half_window, order, deriv)
109
+ padded_array = sg_pad_ends(array, half_window)
110
+ sg_convolve(padded_array, weights)
111
+ end
112
+
113
+ def sg_check_arguments(window_points, order)
114
+ if !window_points.is_a?(Integer) || window_points.abs != window_points || window_points % 2 != 1 || window_points < 1
115
+ raise ArgumentError, "window_points size must be a positive odd integer"
116
+ end
117
+ if !order.is_a?(Integer) || order < 0
118
+ raise ArgumentError, "order must be an integer >= 0"
119
+ end
120
+ if window_points < order + 2
121
+ raise ArgumentError, "window_points is too small for the polynomials order"
122
+ end
123
+ end
124
+
125
+ def sg_convolve(data, weights, mode=:valid)
126
+ data.each_cons(weights.size).map do |ar|
127
+ ar.zip(weights).map {|pair| pair[0] * pair[1] }.reduce(:+)
128
+ end
129
+ end
130
+
131
+ # pads the ends with the reverse, geometric inverse sequence
132
+ def sg_pad_ends(array, half_window)
133
+ start = array[1..half_window]
134
+ start.reverse!
135
+ start.map! {|v| array[0] - (v - array[0]).abs }
136
+
137
+ fin = array[(-half_window-1)...-1]
138
+ fin.reverse!
139
+ fin.map! {|v| array[-1] + (v - array[-1]).abs }
140
+ start.push(*array, *fin)
141
+ end
142
+
143
+ # pads the ends of x vals
144
+ def sg_pad_xvals(array, half_window)
145
+ deltas = array[0..half_window].each_cons(2).map {|a,b| b-a }
146
+ start = array[0]
147
+ prevals = deltas.map do |delta|
148
+ newval = start - delta
149
+ start = newval
150
+ newval
151
+ end
152
+ prevals.reverse!
153
+
154
+ deltas = array[(-half_window-1)..-1].each_cons(2).map {|a,b| b-a }
155
+ start = array[-1]
156
+ postvals = deltas.reverse.map do |delta|
157
+ newval = start + delta
158
+ start = newval
159
+ newval
160
+ end
161
+
162
+ prevals.push(*array, *postvals)
163
+ end
164
+
165
+ # returns an object that will convolve with the padded array
166
+ def sg_weights(half_window, order, deriv=0)
167
+ # byebug
168
+ mat = Matrix[ *(-half_window..half_window).map {|k| (0..order).map {|i| k**i }} ]
169
+ # Moore-Penrose psuedo-inverse without SVD (not so precize)
170
+ # A' = (A.t * A)^-1 * A.t
171
+ pinv_matrix = Matrix[*(mat.transpose*mat).to_a].inverse * Matrix[*mat.to_a].transpose
172
+ pinv = Matrix[*pinv_matrix.to_a]
173
+ pinv.row(deriv).to_a
174
+ end
175
+ end
176
+ end
@@ -1,57 +1,8 @@
1
- require "matrix"
2
-
3
- module Savgol
4
-
5
- def savgol(window_size, order, deriv=0, check_args=false)
6
-
7
- # check arguments
8
- if !window_size.is_a?(Integer) || window_size.abs != window_size || window_size % 2 != 1 || window_size < 1
9
- raise ArgumentError, "window_size size must be a positive odd integer"
10
- end
11
- if !order.is_a?(Integer) || order < 0
12
- raise ArgumentError, "order must be an integer >= 0"
13
- end
14
- if window_size < order + 2
15
- raise ArgumentError, "window_size is too small for the polynomials order"
16
- end
17
-
18
- half_window = (window_size -1) / 2
19
- weights = sg_weights(half_window, order, deriv)
20
- ar = sg_pad_ends(half_window)
21
- sg_convolve(ar, weights)
22
- end
23
-
24
- def sg_convolve(data, weights, mode=:valid)
25
- data.each_cons(weights.size).map do |ar|
26
- ar.zip(weights).map {|pair| pair[0] * pair[1] }.reduce(:+)
27
- end
28
- end
29
-
30
- # pads the ends with the reverse, geometric inverse sequence
31
- def sg_pad_ends(half_window)
32
- start = self[1..half_window]
33
- start.reverse!
34
- start.map! {|v| self[0] - (v - self[0]).abs }
35
-
36
- fin = self[(-half_window-1)...-1]
37
- fin.reverse!
38
- fin.map! {|v| self[-1] + (v - self[-1]).abs }
39
- start.push(*self, *fin)
40
- end
41
-
42
- # returns an object that will convolve with the padded array
43
- def sg_weights(half_window, order, deriv=0)
44
- # byebug
45
- mat = Matrix[ *(-half_window..half_window).map {|k| (0..order).map {|i| k**i }} ]
46
- # Moore-Penrose psuedo-inverse without SVD (not so precize)
47
- # A' = (A.t * A)^-1 * A.t
48
- pinv_matrix = Matrix[*(mat.transpose*mat).to_a].inverse * Matrix[*mat.to_a].transpose
49
- pinv = Matrix[*pinv_matrix.to_a]
50
- pinv.row(deriv).to_a
51
- end
52
-
53
- end
1
+ require 'savgol'
54
2
 
55
3
  class Array
56
- include Savgol
4
+ # see Savgol#savgol
5
+ def savgol(*args)
6
+ Savgol.savgol(self, *args)
7
+ end
57
8
  end
@@ -1,3 +1,3 @@
1
1
  module Savgol
2
- VERSION = "0.2.1"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -30,6 +30,7 @@ Gem::Specification.new do |spec|
30
30
  ["rspec", "~> 2.14.1"],
31
31
  ["rdoc", "~> 4.1.0"],
32
32
  ["simplecov", "~> 0.8.2"],
33
+ ["gnuplot"],
33
34
  ].each do |args|
34
35
  spec.add_development_dependency(*args)
35
36
  end
@@ -5,16 +5,9 @@ shared_examples "a savgol smoother" do
5
5
  object
6
6
  end
7
7
 
8
- it 'pads with the reverse geometrically inverted sequence' do
9
- expect(smoother.sg_pad_ends(2)).to eq [-1, 0, 1, 2, 3, 4, -7, -2, 0, 1, 1, 1, 2]
10
- expect(smoother.sg_pad_ends(3)).to eq [-2, -1, 0, 1, 2, 3, 4, -7, -2, 0, 1, 1, 1, 2, 4]
11
- end
12
-
13
8
  describe 'smoothing a signal' do
14
9
  let(:smoother) do
15
- object = described_class[1, 2, 3, 4, 3.5, 5, 3, 2.2, 3, 0, -1, 2, 0, -2, -5, -8, -7, -2, 0, 1, 1]
16
- object.extend(Savgol)
17
- object
10
+ described_class[1, 2, 3, 4, 3.5, 5, 3, 2.2, 3, 0, -1, 2, 0, -2, -5, -8, -7, -2, 0, 1, 1]
18
11
  end
19
12
 
20
13
  it "works for the simple case" do
@@ -0,0 +1,83 @@
1
+ require 'spec_helper'
2
+
3
+ describe Savgol do
4
+ describe 'padding' do
5
+ let(:array) { [1, 2, 3, 4, -7, -2, 0, 1, 1] }
6
+ it 'pads with the reverse geometrically inverted sequence' do
7
+ expect(Savgol.sg_pad_ends(array, 2)).to eq [-1, 0, 1, 2, 3, 4, -7, -2, 0, 1, 1, 1, 2]
8
+ expect(Savgol.sg_pad_ends(array, 3)).to eq [-2, -1, 0, 1, 2, 3, 4, -7, -2, 0, 1, 1, 1, 2, 4]
9
+ end
10
+
11
+ it 'pads unevenly spaced x vals' do
12
+ ar = %w(-1 0 2 3 4 7 8 10 11 12 13 14 17 18).map &:to_f
13
+ expect(Savgol.sg_pad_xvals(ar, 2)).to eq %w(-4 -2 -1 0 2 3 4 7 8 10 11 12 13 14 17 18 19 22]).map(&:to_f)
14
+ end
15
+ end
16
+
17
+ describe 'finding nearest index' do
18
+ let(:original) { [0,1,2,3,4] }
19
+
20
+ it 'finds nearest index to the left' do
21
+ newvals = [-2, -1]
22
+ expect(Savgol.sg_nearest_index(original, newvals)).to eq([0,0])
23
+ end
24
+
25
+ it 'finds nearest indices in the middle' do
26
+ newvals = [1,1.4,1.5,1.6,2,2.1]
27
+ expect(Savgol.sg_nearest_index(original, newvals)).to eq([1,1,1,2,2,2])
28
+ end
29
+
30
+ it 'finds nearest index to the right' do
31
+ newvals = [5,6]
32
+ expect(Savgol.sg_nearest_index(original, newvals)).to eq([4,4])
33
+ end
34
+
35
+ it 'efficiently finds the nearest index' do
36
+ newvals = [-2,-1,1,2.4,2.5,4,4.5,5]
37
+ expect(Savgol.sg_nearest_index(original, newvals)).to eq([0,0,1,2,2,4,4,4])
38
+ end
39
+ end
40
+
41
+ describe 'smoothing unevenly spaced data' do
42
+ xvals = %w(-1 0 2 3 4 7 8 10 11 12 13 14 17 18).map &:to_f
43
+ new_xvals = xvals.map {|v| v + 0.5 }
44
+ yvals = %w(-2 1 0 1 1 3 4 7 8 9 7 4 1 2).map {|v| v.to_f + 30 }
45
+
46
+ yvals_on = Savgol.savgol_uneven(xvals, yvals, 5, 2)
47
+
48
+ yvals_off = Savgol.savgol_uneven(xvals, yvals, 5, 2, new_xvals: new_xvals)
49
+
50
+ yvals_pretend_even = Savgol.savgol(yvals, 5, 2)
51
+
52
+ if false
53
+ require 'gnuplot'
54
+ Gnuplot.open do |gp|
55
+ Gnuplot::Plot.new(gp) do |plot|
56
+
57
+ plot.data << Gnuplot::DataSet.new([new_xvals, yvals_off]) do |ds|
58
+ ds.title = "x interpolated"
59
+ ds.with = "linespoints"
60
+ end
61
+
62
+ plot.data << Gnuplot::DataSet.new([xvals, yvals_pretend_even]) do |ds|
63
+ ds.title = "pretend evenly spaced"
64
+ ds.with = "linespoints"
65
+ end
66
+
67
+ plot.data << Gnuplot::DataSet.new([xvals, yvals_on]) do |ds|
68
+ ds.title = "smoothed"
69
+ ds.with = "linespoints"
70
+ end
71
+
72
+ plot.data << Gnuplot::DataSet.new([xvals, yvals]) do |ds|
73
+ ds.title = "original"
74
+ ds.with = "points"
75
+ end
76
+
77
+ end
78
+ end
79
+ end
80
+
81
+ end
82
+ end
83
+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: savgol
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John T. Prince
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-03-27 00:00:00.000000000 Z
12
+ date: 2014-04-24 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -81,6 +81,20 @@ dependencies:
81
81
  - - ~>
82
82
  - !ruby/object:Gem::Version
83
83
  version: 0.8.2
84
+ - !ruby/object:Gem::Dependency
85
+ name: gnuplot
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - '>='
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - '>='
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
84
98
  description: Extends Array class with method which calculates applies Savitzky-Golay
85
99
  filter used for smoothing the data
86
100
  email:
@@ -102,6 +116,7 @@ files:
102
116
  - savgol.gemspec
103
117
  - spec/savgol/array_spec.rb
104
118
  - spec/savgol_shared_example.rb
119
+ - spec/savgol_spec.rb
105
120
  - spec/spec_helper.rb
106
121
  homepage: http://github.com/princelab/savgol
107
122
  licenses:
@@ -130,4 +145,5 @@ summary: performs Savitzky-Golay smoothing
130
145
  test_files:
131
146
  - spec/savgol/array_spec.rb
132
147
  - spec/savgol_shared_example.rb
148
+ - spec/savgol_spec.rb
133
149
  - spec/spec_helper.rb