lowess 0.0.1
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 +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
|