lowess 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/README.md +37 -0
- data/Rakefile +11 -0
- data/ext/ext_lowess/README.md +1 -0
- data/ext/ext_lowess/clowess.c +308 -0
- data/ext/ext_lowess/download_clowess.rb +50 -0
- data/ext/ext_lowess/extconf.rb +4 -0
- data/ext/ext_lowess/lowess.c +69 -0
- data/lib/lowess.rb +29 -0
- data/lib/lowess/point.rb +22 -0
- data/lib/lowess/version.rb +3 -0
- data/lowess.gemspec +23 -0
- data/test/test_lowess.rb +155 -0
- metadata +89 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 3f8e62062c82338a4acfc42b4bab9bc873ab8535
|
4
|
+
data.tar.gz: 0b7607b81e80f5574d82f94373438496c8f84829
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 65736717ab720df61c4bd05f3d51c6beaa96710dc340c65f2170b22d7ee36f0345b7647eda3f01b9904cc7c76d8d844a126a588b21aa2b01e7a665f279882c50
|
7
|
+
data.tar.gz: 7e12d26d8d291c78cff4d80349c72294ac16c51b93adc3e19bc6049aebc5e9175461851886ce2b8bfbaf01bbead4beb1bc2dc01d7340487e901e53fe66e68b67
|
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
pollster
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.2.3
|
data/README.md
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
If you've ever used R's [lowess()](https://svn.r-project.org/R/tags/R-3-2-4/src/library/stats/src/lowess.doc)
|
2
|
+
and you wish you could call it from Ruby, this gem's for you.
|
3
|
+
|
4
|
+
# Installation
|
5
|
+
|
6
|
+
`gem install lowess`
|
7
|
+
|
8
|
+
or in your Gemfile:
|
9
|
+
|
10
|
+
`gem 'lowess'`
|
11
|
+
|
12
|
+
# Usage
|
13
|
+
|
14
|
+
```rb
|
15
|
+
require 'lowess'
|
16
|
+
|
17
|
+
points = [
|
18
|
+
Lowess::Point.new(1, 1.1),
|
19
|
+
Lowess::Point.new(1.3, 1.3),
|
20
|
+
Lowess::Point.new(1.7, 1.1),
|
21
|
+
Lowess::Point.new(2.1, 1.1)
|
22
|
+
]
|
23
|
+
|
24
|
+
# returns (1, 1.1531) (1.3, 1.1854) (1.7, 1.3444) (2.1, 1.6363)
|
25
|
+
Lowess::lowess(points, f: 1.0, iter: 4)
|
26
|
+
```
|
27
|
+
|
28
|
+
# Development
|
29
|
+
|
30
|
+
*To run tests*: `rake`
|
31
|
+
|
32
|
+
*To fiddle with C*: look in `ext/ext_lowess`
|
33
|
+
|
34
|
+
# License
|
35
|
+
|
36
|
+
[GPL2](http://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) because the
|
37
|
+
code is downloaded from R automatically.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
clowess.c is copied from R, which makes this whole project GPL
|
@@ -0,0 +1,308 @@
|
|
1
|
+
/*
|
2
|
+
* R : A Computer Langage for Statistical Data Analysis
|
3
|
+
* Copyright (C) 1996 Robert Gentleman and Ross Ihaka
|
4
|
+
* Copyright (C) 1999-20012 The R Core Team
|
5
|
+
*
|
6
|
+
* This program is free software; you can redistribute it and/or modify
|
7
|
+
* it under the terms of the GNU General Public License as published by
|
8
|
+
* the Free Software Foundation; either version 2 of the License, or
|
9
|
+
* (at your option) any later version.
|
10
|
+
*
|
11
|
+
* This program is distributed in the hope that it will be useful,
|
12
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
* GNU General Public License for more details.
|
15
|
+
*
|
16
|
+
* You should have received a copy of the GNU General Public License
|
17
|
+
* along with this program; if not, a copy is available at
|
18
|
+
* https://www.R-project.org/Licenses/
|
19
|
+
*/
|
20
|
+
|
21
|
+
#ifdef HAVE_CONFIG_H
|
22
|
+
# include <config.h>
|
23
|
+
#endif
|
24
|
+
|
25
|
+
#ifdef ENABLE_NLS
|
26
|
+
#include <libintl.h>
|
27
|
+
#define _(String) dgettext ("stats", String)
|
28
|
+
#else
|
29
|
+
#define _(String) (String)
|
30
|
+
#endif
|
31
|
+
|
32
|
+
#include <math.h>
|
33
|
+
double fmax2(double x, double y) { return (x < y) ? y : x; }
|
34
|
+
int imin2(int x, int y) { return (x < y) ? x : y; }
|
35
|
+
int imax2(int x, int y) { return (x < y) ? y : x; }
|
36
|
+
/* fmax2, imin2, imax2 */
|
37
|
+
#define R_INLINE inline /* prototypes for lowess and clowess */
|
38
|
+
typedef enum { FALSE = 0, TRUE /*, MAYBE */ } Rboolean;
|
39
|
+
|
40
|
+
static void rPsort2(double *x, int lo, int hi, int k)
|
41
|
+
{
|
42
|
+
double v, w;
|
43
|
+
int L, R, i, j;
|
44
|
+
|
45
|
+
for (L = lo, R = hi; L < R; ) {
|
46
|
+
v = x[k];
|
47
|
+
for(i = L, j = R; i <= j;) {
|
48
|
+
while (x[i] < v) i++;
|
49
|
+
while (v < x[j]) j--;
|
50
|
+
if (i <= j) { w = x[i]; x[i++] = x[j]; x[j--] = w; }
|
51
|
+
}
|
52
|
+
if (j < k) L = i;
|
53
|
+
if (k < i) R = j;
|
54
|
+
}
|
55
|
+
}
|
56
|
+
|
57
|
+
static void rPsort(double *x, int n, int k)
|
58
|
+
{
|
59
|
+
rPsort2(x, 0, n-1, k);
|
60
|
+
}
|
61
|
+
/* rPsort() */
|
62
|
+
#ifdef DEBUG_lowess
|
63
|
+
# include <R_ext/Print.h>
|
64
|
+
#endif
|
65
|
+
|
66
|
+
static R_INLINE double fsquare(double x)
|
67
|
+
{
|
68
|
+
return x * x;
|
69
|
+
}
|
70
|
+
|
71
|
+
static R_INLINE double fcube(double x)
|
72
|
+
{
|
73
|
+
return x * x * x;
|
74
|
+
}
|
75
|
+
|
76
|
+
static void lowest(double *x, double *y, int n, double *xs, double *ys,
|
77
|
+
int nleft, int nright, double *w,
|
78
|
+
Rboolean userw, double *rw, Rboolean *ok)
|
79
|
+
{
|
80
|
+
int nrt, j;
|
81
|
+
double a, b, c, h, h1, h9, r, range;
|
82
|
+
|
83
|
+
x--;
|
84
|
+
y--;
|
85
|
+
w--;
|
86
|
+
rw--;
|
87
|
+
|
88
|
+
range = x[n]-x[1];
|
89
|
+
h = fmax2(*xs-x[nleft], x[nright]-*xs);
|
90
|
+
h9 = 0.999*h;
|
91
|
+
h1 = 0.001*h;
|
92
|
+
|
93
|
+
/* sum of weights */
|
94
|
+
|
95
|
+
a = 0.;
|
96
|
+
j = nleft;
|
97
|
+
while (j <= n) {
|
98
|
+
|
99
|
+
/* compute weights */
|
100
|
+
/* (pick up all ties on right) */
|
101
|
+
|
102
|
+
w[j] = 0.;
|
103
|
+
r = fabs(x[j] - *xs);
|
104
|
+
if (r <= h9) {
|
105
|
+
if (r <= h1)
|
106
|
+
w[j] = 1.;
|
107
|
+
else
|
108
|
+
w[j] = fcube(1.-fcube(r/h));
|
109
|
+
if (userw)
|
110
|
+
w[j] *= rw[j];
|
111
|
+
a += w[j];
|
112
|
+
}
|
113
|
+
else if (x[j] > *xs)
|
114
|
+
break;
|
115
|
+
j = j+1;
|
116
|
+
}
|
117
|
+
|
118
|
+
/* rightmost pt (may be greater */
|
119
|
+
/* than nright because of ties) */
|
120
|
+
|
121
|
+
nrt = j-1;
|
122
|
+
if (a <= 0.)
|
123
|
+
*ok = FALSE;
|
124
|
+
else {
|
125
|
+
*ok = TRUE;
|
126
|
+
|
127
|
+
/* weighted least squares */
|
128
|
+
/* make sum of w[j] == 1 */
|
129
|
+
|
130
|
+
for(j=nleft ; j<=nrt ; j++)
|
131
|
+
w[j] /= a;
|
132
|
+
if (h > 0.) {
|
133
|
+
a = 0.;
|
134
|
+
|
135
|
+
/* use linear fit */
|
136
|
+
/* weighted center of x values */
|
137
|
+
|
138
|
+
for(j=nleft ; j<=nrt ; j++)
|
139
|
+
a += w[j] * x[j];
|
140
|
+
b = *xs - a;
|
141
|
+
c = 0.;
|
142
|
+
for(j=nleft ; j<=nrt ; j++)
|
143
|
+
c += w[j]*fsquare(x[j]-a);
|
144
|
+
if (sqrt(c) > 0.001*range) {
|
145
|
+
b /= c;
|
146
|
+
|
147
|
+
/* points are spread out */
|
148
|
+
/* enough to compute slope */
|
149
|
+
|
150
|
+
for(j=nleft; j <= nrt; j++)
|
151
|
+
w[j] *= (b*(x[j]-a) + 1.);
|
152
|
+
}
|
153
|
+
}
|
154
|
+
*ys = 0.;
|
155
|
+
for(j=nleft; j <= nrt; j++)
|
156
|
+
*ys += w[j] * y[j];
|
157
|
+
}
|
158
|
+
}
|
159
|
+
|
160
|
+
void clowess(double *x, double *y, int n,
|
161
|
+
double f, int nsteps, double delta,
|
162
|
+
double *ys, double *rw, double *res)
|
163
|
+
{
|
164
|
+
int i, iter, j, last, m1, m2, nleft, nright, ns;
|
165
|
+
Rboolean ok;
|
166
|
+
double alpha, c1, c9, cmad, cut, d1, d2, denom, r, sc;
|
167
|
+
|
168
|
+
if (n < 2) {
|
169
|
+
ys[0] = y[0]; return;
|
170
|
+
}
|
171
|
+
|
172
|
+
/* nleft, nright, last, etc. must all be shifted to get rid of these: */
|
173
|
+
x--;
|
174
|
+
y--;
|
175
|
+
ys--;
|
176
|
+
|
177
|
+
|
178
|
+
/* at least two, at most n points */
|
179
|
+
ns = imax2(2, imin2(n, (int)(f*n + 1e-7)));
|
180
|
+
#ifdef DEBUG_lowess
|
181
|
+
REprintf("lowess(): ns = %d\n", ns);
|
182
|
+
#endif
|
183
|
+
|
184
|
+
/* robustness iterations */
|
185
|
+
|
186
|
+
iter = 1;
|
187
|
+
while (iter <= nsteps+1) {
|
188
|
+
nleft = 1;
|
189
|
+
nright = ns;
|
190
|
+
last = 0; /* index of prev estimated point */
|
191
|
+
i = 1; /* index of current point */
|
192
|
+
|
193
|
+
for(;;) {
|
194
|
+
if (nright < n) {
|
195
|
+
|
196
|
+
/* move nleft, nright to right */
|
197
|
+
/* if radius decreases */
|
198
|
+
|
199
|
+
d1 = x[i] - x[nleft];
|
200
|
+
d2 = x[nright+1] - x[i];
|
201
|
+
|
202
|
+
/* if d1 <= d2 with */
|
203
|
+
/* x[nright+1] == x[nright], */
|
204
|
+
/* lowest fixes */
|
205
|
+
|
206
|
+
if (d1 > d2) {
|
207
|
+
|
208
|
+
/* radius will not */
|
209
|
+
/* decrease by */
|
210
|
+
/* move right */
|
211
|
+
|
212
|
+
nleft++;
|
213
|
+
nright++;
|
214
|
+
continue;
|
215
|
+
}
|
216
|
+
}
|
217
|
+
|
218
|
+
/* fitted value at x[i] */
|
219
|
+
|
220
|
+
lowest(&x[1], &y[1], n, &x[i], &ys[i],
|
221
|
+
nleft, nright, res, iter>1, rw, &ok);
|
222
|
+
if (!ok) ys[i] = y[i];
|
223
|
+
|
224
|
+
/* all weights zero */
|
225
|
+
/* copy over value (all rw==0) */
|
226
|
+
|
227
|
+
if (last < i-1) {
|
228
|
+
denom = x[i]-x[last];
|
229
|
+
|
230
|
+
/* skipped points -- interpolate */
|
231
|
+
/* non-zero - proof? */
|
232
|
+
|
233
|
+
for(j = last+1; j < i; j++) {
|
234
|
+
alpha = (x[j]-x[last])/denom;
|
235
|
+
ys[j] = alpha*ys[i] + (1.-alpha)*ys[last];
|
236
|
+
}
|
237
|
+
}
|
238
|
+
|
239
|
+
/* last point actually estimated */
|
240
|
+
last = i;
|
241
|
+
|
242
|
+
/* x coord of close points */
|
243
|
+
cut = x[last]+delta;
|
244
|
+
for (i = last+1; i <= n; i++) {
|
245
|
+
if (x[i] > cut)
|
246
|
+
break;
|
247
|
+
if (x[i] == x[last]) {
|
248
|
+
ys[i] = ys[last];
|
249
|
+
last = i;
|
250
|
+
}
|
251
|
+
}
|
252
|
+
i = imax2(last+1, i-1);
|
253
|
+
if (last >= n)
|
254
|
+
break;
|
255
|
+
}
|
256
|
+
/* residuals */
|
257
|
+
for(i = 0; i < n; i++)
|
258
|
+
res[i] = y[i+1] - ys[i+1];
|
259
|
+
|
260
|
+
/* overall scale estimate */
|
261
|
+
sc = 0.;
|
262
|
+
for(i = 0; i < n; i++) sc += fabs(res[i]);
|
263
|
+
sc /= n;
|
264
|
+
|
265
|
+
/* compute robustness weights */
|
266
|
+
/* except last time */
|
267
|
+
|
268
|
+
if (iter > nsteps)
|
269
|
+
break;
|
270
|
+
/* Note: The following code, biweight_{6 MAD|Ri|}
|
271
|
+
is also used in stl(), loess and several other places.
|
272
|
+
--> should provide API here (MM) */
|
273
|
+
for(i = 0 ; i < n ; i++)
|
274
|
+
rw[i] = fabs(res[i]);
|
275
|
+
|
276
|
+
/* Compute cmad := 6 * median(rw[], n) ---- */
|
277
|
+
/* FIXME: We need C API in R for Median ! */
|
278
|
+
m1 = n/2;
|
279
|
+
/* partial sort, for m1 & m2 */
|
280
|
+
rPsort(rw, n, m1);
|
281
|
+
if(n % 2 == 0) {
|
282
|
+
m2 = n-m1-1;
|
283
|
+
rPsort(rw, n, m2);
|
284
|
+
cmad = 3.*(rw[m1]+rw[m2]);
|
285
|
+
}
|
286
|
+
else { /* n odd */
|
287
|
+
cmad = 6.*rw[m1];
|
288
|
+
}
|
289
|
+
#ifdef DEBUG_lowess
|
290
|
+
REprintf(" cmad = %12g\n", cmad);
|
291
|
+
#endif
|
292
|
+
if(cmad < 1e-7 * sc) /* effectively zero */
|
293
|
+
break;
|
294
|
+
c9 = 0.999*cmad;
|
295
|
+
c1 = 0.001*cmad;
|
296
|
+
for(i = 0 ; i < n ; i++) {
|
297
|
+
r = fabs(res[i]);
|
298
|
+
if (r <= c1)
|
299
|
+
rw[i] = 1.;
|
300
|
+
else if (r <= c9)
|
301
|
+
rw[i] = fsquare(1.-fsquare(r/cmad));
|
302
|
+
else
|
303
|
+
rw[i] = 0.;
|
304
|
+
}
|
305
|
+
iter++;
|
306
|
+
}
|
307
|
+
}
|
308
|
+
|
@@ -0,0 +1,50 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'open-uri'
|
4
|
+
|
5
|
+
clowess = open('https://svn.r-project.org/R/tags/R-3-2-4/src/library/stats/src/lowess.c') { |f| f.read }
|
6
|
+
|
7
|
+
rmath_stub = <<EOT
|
8
|
+
double fmax2(double x, double y) { return (x < y) ? y : x; }
|
9
|
+
int imin2(int x, int y) { return (x < y) ? x : y; }
|
10
|
+
int imax2(int x, int y) { return (x < y) ? y : x; }
|
11
|
+
EOT
|
12
|
+
|
13
|
+
boolean_stub = <<EOT
|
14
|
+
typedef enum { FALSE = 0, TRUE /*, MAYBE */ } Rboolean;
|
15
|
+
EOT
|
16
|
+
|
17
|
+
# From https://svn.r-project.org/R/tags/R-3-2-4/src/main/sort.c
|
18
|
+
rpsort_stub = <<EOT
|
19
|
+
static void rPsort2(double *x, int lo, int hi, int k)
|
20
|
+
{
|
21
|
+
double v, w;
|
22
|
+
int L, R, i, j;
|
23
|
+
|
24
|
+
for (L = lo, R = hi; L < R; ) {
|
25
|
+
v = x[k];
|
26
|
+
for(i = L, j = R; i <= j;) {
|
27
|
+
while (x[i] < v) i++;
|
28
|
+
while (v < x[j]) j--;
|
29
|
+
if (i <= j) { w = x[i]; x[i++] = x[j]; x[j--] = w; }
|
30
|
+
}
|
31
|
+
if (j < k) L = i;
|
32
|
+
if (k < i) R = j;
|
33
|
+
}
|
34
|
+
}
|
35
|
+
|
36
|
+
static void rPsort(double *x, int n, int k)
|
37
|
+
{
|
38
|
+
rPsort2(x, 0, n-1, k);
|
39
|
+
}
|
40
|
+
EOT
|
41
|
+
|
42
|
+
out = clowess
|
43
|
+
.sub(/#include <Rmath\.h>/, rmath_stub)
|
44
|
+
.sub(/#include <R_ext\/Applic\.h>/, '#define R_INLINE inline')
|
45
|
+
.sub(/#include <R_ext\/Boolean\.h>/, boolean_stub)
|
46
|
+
.sub(/#include <R_ext\/Utils\.h>/, rpsort_stub)
|
47
|
+
.sub(/static\r?\nvoid clowess/, 'void clowess')
|
48
|
+
.sub(/#include <Rinternals.*/m, '') # drop the R wrapper
|
49
|
+
|
50
|
+
open('clowess.c', 'w') { |f| f.write(out) }
|
@@ -0,0 +1,69 @@
|
|
1
|
+
#include <ruby.h>
|
2
|
+
|
3
|
+
static VALUE Lowess;
|
4
|
+
static VALUE Lowess_Point;
|
5
|
+
|
6
|
+
static ID X;
|
7
|
+
static ID Y;
|
8
|
+
static ID Point;
|
9
|
+
|
10
|
+
void clowess(double *x, double *y, int n,
|
11
|
+
double f, int nsteps, double delta,
|
12
|
+
double *ys, double *rw, double *res);
|
13
|
+
|
14
|
+
static VALUE
|
15
|
+
lowess_ext_lowess(VALUE self, VALUE in_points, VALUE in_f, VALUE in_iter, VALUE in_delta) {
|
16
|
+
const int len = RARRAY_LEN(in_points);
|
17
|
+
|
18
|
+
double* xs = ALLOC_N(double, len);
|
19
|
+
double* ys = ALLOC_N(double, len);
|
20
|
+
|
21
|
+
const double f = NUM2DBL(in_f);
|
22
|
+
const int iter = NUM2INT(in_iter);
|
23
|
+
const double delta = NUM2DBL(in_delta);
|
24
|
+
|
25
|
+
double* out_ys = ALLOC_N(double, len);
|
26
|
+
double* rw = ALLOC_N(double, len);
|
27
|
+
double* res = ALLOC_N(double, len);
|
28
|
+
VALUE* points = ALLOC_N(VALUE, len);
|
29
|
+
VALUE point_args[2];
|
30
|
+
VALUE ret = rb_ary_new_capa(len);
|
31
|
+
int i;
|
32
|
+
|
33
|
+
VALUE e;
|
34
|
+
for (i = 0; i < len; i++) {
|
35
|
+
e = rb_ary_entry(in_points, i);
|
36
|
+
xs[i] = NUM2DBL(rb_ivar_get(e, X));
|
37
|
+
ys[i] = NUM2DBL(rb_ivar_get(e, Y));
|
38
|
+
}
|
39
|
+
|
40
|
+
clowess(xs, ys, len, f, iter, delta, out_ys, rw, res);
|
41
|
+
|
42
|
+
for (i = 0; i < len; i++) {
|
43
|
+
point_args[0] = DBL2NUM(xs[i]);
|
44
|
+
point_args[1] = DBL2NUM(out_ys[i]);
|
45
|
+
points[i] = rb_class_new_instance(2, point_args, Lowess_Point);
|
46
|
+
}
|
47
|
+
|
48
|
+
rb_ary_cat(ret, points, len);
|
49
|
+
|
50
|
+
xfree(points);
|
51
|
+
xfree(res);
|
52
|
+
xfree(rw);
|
53
|
+
xfree(out_ys);
|
54
|
+
xfree(ys);
|
55
|
+
xfree(xs);
|
56
|
+
|
57
|
+
return ret;
|
58
|
+
}
|
59
|
+
|
60
|
+
void Init_ext_lowess() {
|
61
|
+
X = rb_intern("@x");
|
62
|
+
Y = rb_intern("@y");
|
63
|
+
Point = rb_intern("Point");
|
64
|
+
|
65
|
+
Lowess = rb_define_module("Lowess");
|
66
|
+
Lowess_Point = rb_const_get(Lowess, Point);
|
67
|
+
|
68
|
+
rb_define_singleton_method(Lowess, "ext_lowess", lowess_ext_lowess, 4);
|
69
|
+
}
|
data/lib/lowess.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# A LOWESS smoother
|
2
|
+
module Lowess
|
3
|
+
# Smooths the input points
|
4
|
+
#
|
5
|
+
# Arguments:
|
6
|
+
#
|
7
|
+
# * points: Array of Lowess::Point objects, in any order.
|
8
|
+
# * options: any of:
|
9
|
+
# * f: the smoother span. Larger values give more smoothness.
|
10
|
+
# * iter: the number of 'robustifying' iterations. Smaller is faster.
|
11
|
+
# * delta: minimum x distance between input points. Larger is faster.
|
12
|
+
#
|
13
|
+
# See https://stat.ethz.ch/R-manual/R-devel/library/stats/html/lowess.html
|
14
|
+
# for more complete documentation.
|
15
|
+
def self.lowess(points, options={})
|
16
|
+
raise ArgumentError.new("Must pass one or more points") if points.empty?
|
17
|
+
|
18
|
+
sorted_points = points.sort
|
19
|
+
|
20
|
+
f = (options[:f] || 2.0 / 3).to_f
|
21
|
+
iter = (options[:iter] || 3).to_i
|
22
|
+
delta = (options[:delta] || (sorted_points.last.x - sorted_points.first.x).to_f / 100).to_f || 1.0
|
23
|
+
|
24
|
+
ext_lowess(points, f, iter, delta)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
require 'lowess/point'
|
29
|
+
require 'ext_lowess'
|
data/lib/lowess/point.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
class Lowess::Point
|
2
|
+
include Comparable
|
3
|
+
|
4
|
+
attr_reader(:x, :y)
|
5
|
+
|
6
|
+
def initialize(x, y)
|
7
|
+
@x = x.to_f
|
8
|
+
@y = y.to_f
|
9
|
+
end
|
10
|
+
|
11
|
+
def <=>(p2)
|
12
|
+
if x != p2.x
|
13
|
+
x <=> p2.x
|
14
|
+
else
|
15
|
+
y <=> p2.y
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
sprintf("(%0.4f, %0.4f)", @x, @y)
|
21
|
+
end
|
22
|
+
end
|
data/lowess.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
$:.push File.expand_path('../lib', __FILE__)
|
2
|
+
require 'lowess/version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = 'lowess'
|
6
|
+
s.version = Lowess::VERSION
|
7
|
+
s.platform = Gem::Platform::RUBY
|
8
|
+
s.authors = [ 'Adam Hooper' ]
|
9
|
+
s.email = [ 'adam@adamhooper.com' ]
|
10
|
+
s.homepage = 'http://rubygems.org/gems/ruby-lowess'
|
11
|
+
s.summary = 'Lowess scatter plot smoothing'
|
12
|
+
s.description = 'Runs the Lowess smoothing algorithm'
|
13
|
+
s.license = 'GPL2'
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- test/*`.split("\n")
|
17
|
+
|
18
|
+
s.require_paths = [ 'lib' ]
|
19
|
+
s.extensions = [ 'ext/ext_lowess/extconf.rb' ]
|
20
|
+
|
21
|
+
s.add_development_dependency 'rake', '~> 11'
|
22
|
+
s.add_development_dependency 'rake-compiler', '~> 0.9'
|
23
|
+
end
|
data/test/test_lowess.rb
ADDED
@@ -0,0 +1,155 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'lowess'
|
3
|
+
|
4
|
+
class LowessTest < Minitest::Test
|
5
|
+
def test_typical_input
|
6
|
+
# R session:
|
7
|
+
#
|
8
|
+
# > x <- c(0, 1, 2, 3, 4)
|
9
|
+
# > y <- c(2, 3, 1, 5, 4)
|
10
|
+
# > lowess(x=x, y=y, f=1)
|
11
|
+
# $x
|
12
|
+
# [1] 0 1 2 3 4
|
13
|
+
#
|
14
|
+
# $y
|
15
|
+
# [1] 2.102895 2.549205 3.183370 3.727202 4.376544
|
16
|
+
#
|
17
|
+
out = Lowess::lowess([ p(0, 2), p(1, 3), p(2, 1), p(3, 5), p(4, 4) ], f: 1)
|
18
|
+
assert_near [ p(0, 2.102895), p(1, 2.549205), p(2, 3.183370), p(3, 3.727202), p(4, 4.376544) ], out
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_zero_inputs
|
22
|
+
# R session:
|
23
|
+
#
|
24
|
+
# > x <- c()
|
25
|
+
# > y <- c()
|
26
|
+
# > lowess(x=x, y=y)
|
27
|
+
# Error in lowess(x, y) : invalid input
|
28
|
+
assert_raises ::ArgumentError do
|
29
|
+
Lowess::lowess([])
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_one_input
|
34
|
+
# R session:
|
35
|
+
#
|
36
|
+
# > x <- c(0)
|
37
|
+
# > y <- c(1)
|
38
|
+
# > lowess(x=x, y=y)
|
39
|
+
# $x
|
40
|
+
# [1] 0
|
41
|
+
#
|
42
|
+
# $y
|
43
|
+
# [1] 1
|
44
|
+
out = Lowess::lowess([ p(0, 1) ])
|
45
|
+
assert_equal [ p(0, 1) ], out
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_two_inputs
|
49
|
+
# R session:
|
50
|
+
#
|
51
|
+
# > x <- c(0, 1)
|
52
|
+
# > y <- c(0.2, 0.2)
|
53
|
+
# > lowess(x, y)
|
54
|
+
# $x
|
55
|
+
# [1] 0 1
|
56
|
+
#
|
57
|
+
# $y
|
58
|
+
# [1] 0.2 0.2
|
59
|
+
input = [ p(0, 0.2), p(1, 0.2) ]
|
60
|
+
assert_equal input, Lowess::lowess(input)
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_f
|
64
|
+
# R session:
|
65
|
+
#
|
66
|
+
# > x <- c(0, 1, 2, 3, 4)
|
67
|
+
# > y <- c(2, 3, 1, 5, 4)
|
68
|
+
# > lowess(x, y, f=0.8)
|
69
|
+
# $x
|
70
|
+
# [1] 0 1 2 3 4
|
71
|
+
#
|
72
|
+
# $y
|
73
|
+
# [1] 2.314658 2.180697 2.847178 3.562879 4.498597
|
74
|
+
assert_near ps(%w(2.314658 2.180697 2.847178 3.562879 4.498597)), Lowess::lowess(ps([ 2, 3, 1, 5, 4 ]), f: 0.8)
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_iter
|
78
|
+
# R session:
|
79
|
+
#
|
80
|
+
# > x <- c(0, 1, 2, 3, 4)
|
81
|
+
# > y <- c(2, 3, 1, 5, 4)
|
82
|
+
# > lowess(x, y, f=0.8, iter=1)
|
83
|
+
# $x
|
84
|
+
# [1] 0 1 2 3 4
|
85
|
+
#
|
86
|
+
# $y
|
87
|
+
# [1] 2.322932 2.167876 2.809222 3.553910 4.517957
|
88
|
+
assert_near ps(%w(2.322932 2.167876 2.809222 3.553910 4.517957)), Lowess::lowess(ps([ 2, 3, 1, 5, 4 ]), f: 0.8, iter: 1)
|
89
|
+
end
|
90
|
+
|
91
|
+
def test_delta
|
92
|
+
# R session:
|
93
|
+
#
|
94
|
+
# > x <- c(0, 1, 2, 3, 4)
|
95
|
+
# > y <- c(2, 3, 1, 5, 4)
|
96
|
+
# > lowess(x, y, f=0.9, delta=2.0)
|
97
|
+
# $x
|
98
|
+
# [1] 0 1 2 3 4
|
99
|
+
#
|
100
|
+
# $y
|
101
|
+
# [1] 2 3 4 4 4
|
102
|
+
assert_near ps(%w(2 3 4 4 4)), Lowess::lowess(ps([ 2, 3, 1, 5, 4 ]), f: 0.8, delta: 2.0)
|
103
|
+
end
|
104
|
+
|
105
|
+
def test_non_uniform_line
|
106
|
+
# R session:
|
107
|
+
#
|
108
|
+
# > x <- c(1, 1.1, 2, 2, 3)
|
109
|
+
# > y <- c(1.1, 1.2, 2.2, 1.2, 3.1)
|
110
|
+
# > lowess(x, y)
|
111
|
+
# $x
|
112
|
+
# [1] 1.0 1.1 2.0 2.0 3.0
|
113
|
+
#
|
114
|
+
# $y
|
115
|
+
# [1] 1.1 1.2 1.7 1.7 3.1
|
116
|
+
input = [ p(1, 1.1), p(1.1, 1.2), p(2, 2.2), p(2, 1.2), p(3, 3.1) ]
|
117
|
+
expect_output = [ p(1, 1.1), p(1.1, 1.2), p(2, 1.7), p(2, 1.7), p(3, 3.1) ]
|
118
|
+
assert_near expect_output, Lowess::lowess(input)
|
119
|
+
end
|
120
|
+
|
121
|
+
def test_example_from_readme
|
122
|
+
points = [
|
123
|
+
Lowess::Point.new(1, 1.1),
|
124
|
+
Lowess::Point.new(1.3, 1.3),
|
125
|
+
Lowess::Point.new(1.7, 1.1),
|
126
|
+
Lowess::Point.new(2.1, 1.7)
|
127
|
+
]
|
128
|
+
|
129
|
+
expect = [ 1, 1.3, 1.7, 2.1 ].zip([ 1.1531, 1.1854, 1.3444, 1.6363]).map { |x, y| Lowess::Point.new(x, y) }
|
130
|
+
|
131
|
+
assert_near expect, Lowess::lowess(points, f: 1.0, iter: 4)
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
def assert_near(pts1, pts2)
|
137
|
+
xs1 = pts1.map { |pt| pt.x.round(4) }
|
138
|
+
xs2 = pts2.map { |pt| pt.x.round(4) }
|
139
|
+
ys1 = pts1.map { |pt| pt.y.round(4) }
|
140
|
+
ys2 = pts2.map { |pt| pt.y.round(4) }
|
141
|
+
|
142
|
+
rounded1 = xs1.zip(ys1).map { |x, y| Lowess::Point.new(x, y) }
|
143
|
+
rounded2 = xs2.zip(ys2).map { |x, y| Lowess::Point.new(x, y) }
|
144
|
+
|
145
|
+
assert_equal rounded1, rounded2
|
146
|
+
end
|
147
|
+
|
148
|
+
def ps(ys)
|
149
|
+
ys.each_with_index.map { |y, i| Lowess::Point.new(i, y) }
|
150
|
+
end
|
151
|
+
|
152
|
+
def p(x, y)
|
153
|
+
Lowess::Point.new(x, y)
|
154
|
+
end
|
155
|
+
end
|
metadata
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: lowess
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Adam Hooper
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-04-07 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '11'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '11'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake-compiler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.9'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.9'
|
41
|
+
description: Runs the Lowess smoothing algorithm
|
42
|
+
email:
|
43
|
+
- adam@adamhooper.com
|
44
|
+
executables: []
|
45
|
+
extensions:
|
46
|
+
- ext/ext_lowess/extconf.rb
|
47
|
+
extra_rdoc_files: []
|
48
|
+
files:
|
49
|
+
- ".gitignore"
|
50
|
+
- ".ruby-gemset"
|
51
|
+
- ".ruby-version"
|
52
|
+
- README.md
|
53
|
+
- Rakefile
|
54
|
+
- ext/ext_lowess/README.md
|
55
|
+
- ext/ext_lowess/clowess.c
|
56
|
+
- ext/ext_lowess/download_clowess.rb
|
57
|
+
- ext/ext_lowess/extconf.rb
|
58
|
+
- ext/ext_lowess/lowess.c
|
59
|
+
- lib/lowess.rb
|
60
|
+
- lib/lowess/point.rb
|
61
|
+
- lib/lowess/version.rb
|
62
|
+
- lowess.gemspec
|
63
|
+
- test/test_lowess.rb
|
64
|
+
homepage: http://rubygems.org/gems/ruby-lowess
|
65
|
+
licenses:
|
66
|
+
- GPL2
|
67
|
+
metadata: {}
|
68
|
+
post_install_message:
|
69
|
+
rdoc_options: []
|
70
|
+
require_paths:
|
71
|
+
- lib
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
requirements: []
|
83
|
+
rubyforge_project:
|
84
|
+
rubygems_version: 2.4.8
|
85
|
+
signing_key:
|
86
|
+
specification_version: 4
|
87
|
+
summary: Lowess scatter plot smoothing
|
88
|
+
test_files:
|
89
|
+
- test/test_lowess.rb
|