1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327 | 1x
84x
84x
84x
372x
336x
336x
336x
84x
84x
336x
336x
84x
336x
336x
336x
84x
36x
36x
36x
36x
120x
120x
152x
124x
120x
392x
392x
392x
120x
152x
632x
36x
120x
120x
152x
120x
36x
1275x
1275x
1x
20x
20x
20x
76x
68x
68x
68x
68x
68x
68x
20x
20x
20x
20x
81x
76x
20x
12x
12x
48x
40x
40x
40x
40x
40x
40x
12x
12x
12x
12x
12x
51x
48x
12x
8x
8x
8x
32x
24x
24x
24x
24x
8x
8x
8x
34x
32x
8x
8x
8x
8x
48x
40x
40x
40x
40x
8x
8x
8x
8x
50x
48x
8x
36x
36x
36x
36x
36x
36x
36x
120x
576x
560x
120x
120x
120x
120x
424x
2080x
2016x
424x
424x
120x
36x
120x
177x
606x
168x
36x
36x
120x
48x
72x
36x
36x
36x
5x
84x
1x
1x
| const DEFAULT_OPTIONS = { order: 2, precision: 2, period: null };
/**
* Determine the coefficient of determination (r^2) of a fit from the observations
* and predictions.
*
* @param {Array<Array<number>>} data - Pairs of observed x-y values
* @param {Array<Array<number>>} results - Pairs of observed predicted x-y values
*
* @return {number} - The r^2 value, or NaN if one cannot be calculated.
*/
function determinationCoefficient(data, results) {
const predictions = [];
const observations = [];
data.forEach((d, i) => {
if (d[1] !== null) {
observations.push(d);
predictions.push(results[i]);
}
});
const sum = observations.reduce((a, observation) => a + observation[1], 0);
const mean = sum / observations.length;
const ssyy = observations.reduce((a, observation) => {
const difference = observation[1] - mean;
return a + (difference * difference);
}, 0);
const sse = observations.reduce((accum, observation, index) => {
const prediction = predictions[index];
const residual = observation[1] - prediction[1];
return accum + (residual * residual);
}, 0);
return 1 - (sse / ssyy);
}
/**
* Determine the solution of a system of linear equations A * x = b using
* Gaussian elimination.
*
* @param {Array<Array<number>>} input - A 2-d matrix of data in row-major form [ A | b ]
* @param {number} order - How many degrees to solve for
*
* @return {Array<number>} - Vector of normalized solution coefficients matrix (x)
*/
function gaussianElimination(input, order) {
const matrix = input;
const n = input.length - 1;
const coefficients = [order];
for (let i = 0; i < n; i++) {
let maxrow = i;
for (let j = i + 1; j < n; j++) {
if (Math.abs(matrix[i][j]) > Math.abs(matrix[i][maxrow])) {
maxrow = j;
}
}
for (let k = i; k < n + 1; k++) {
const tmp = matrix[k][i];
matrix[k][i] = matrix[k][maxrow];
matrix[k][maxrow] = tmp;
}
for (let j = i + 1; j < n; j++) {
for (let k = n; k >= i; k--) {
matrix[k][j] -= (matrix[k][i] * matrix[i][j]) / matrix[i][i];
}
}
}
for (let j = n - 1; j >= 0; j--) {
let total = 0;
for (let k = j + 1; k < n; k++) {
total += matrix[k][j] * coefficients[k];
}
coefficients[j] = (matrix[n][j] - total) / matrix[j][j];
}
return coefficients;
}
/**
* Round a number to a precision, specificed in number of decimal places
*
* @param {number} number - The number to round
* @param {number} precision - The number of decimal places to round to:
* > 0 means decimals, < 0 means powers of 10
*
*
* @return {numbr} - The number, rounded
*/
function round(number, precision) {
const factor = 10 ** precision;
return Math.round(number * factor) / factor;
}
/**
* The set of all fitting methods
*
* @namespace
*/
const methods = {
linear(data, options) {
const sum = [0, 0, 0, 0, 0];
let len = 0;
for (let n = 0; n < data.length; n++) {
if (data[n][1] !== null) {
len++;
sum[0] += data[n][0];
sum[1] += data[n][1];
sum[2] += data[n][0] * data[n][0];
sum[3] += data[n][0] * data[n][1];
sum[4] += data[n][1] * data[n][1];
}
}
const run = ((len * sum[2]) - (sum[0] * sum[0]));
const rise = ((len * sum[3]) - (sum[0] * sum[1]));
const gradient = run === 0 ? 0 : round(rise / run, options.precision);
const intercept = round((sum[1] / len) - ((gradient * sum[0]) / len), options.precision);
const predict = x => ([
round(x, options.precision),
round((gradient * x) + intercept, options.precision)]
);
const points = data.map(point => predict(point[0]));
return {
points,
predict,
equation: [gradient, intercept],
r2: round(determinationCoefficient(data, points), options.precision),
string: intercept === 0 ? `y = ${gradient}x` : `y = ${gradient}x + ${intercept}`,
};
},
exponential(data, options) {
const sum = [0, 0, 0, 0, 0, 0];
for (let n = 0; n < data.length; n++) {
if (data[n][1] !== null) {
sum[0] += data[n][0];
sum[1] += data[n][1];
sum[2] += data[n][0] * data[n][0] * data[n][1];
sum[3] += data[n][1] * Math.log(data[n][1]);
sum[4] += data[n][0] * data[n][1] * Math.log(data[n][1]);
sum[5] += data[n][0] * data[n][1];
}
}
const denominator = ((sum[1] * sum[2]) - (sum[5] * sum[5]));
const a = Math.exp(((sum[2] * sum[3]) - (sum[5] * sum[4])) / denominator);
const b = ((sum[1] * sum[4]) - (sum[5] * sum[3])) / denominator;
const coeffA = round(a, options.precision);
const coeffB = round(b, options.precision);
const predict = x => ([
round(x, options.precision),
round(coeffA * Math.exp(coeffB * x), options.precision),
]);
const points = data.map(point => predict(point[0]));
return {
points,
predict,
equation: [coeffA, coeffB],
string: `y = ${coeffA}e^(${coeffB}x)`,
r2: round(determinationCoefficient(data, points), options.precision),
};
},
logarithmic(data, options) {
const sum = [0, 0, 0, 0];
const len = data.length;
for (let n = 0; n < len; n++) {
if (data[n][1] !== null) {
sum[0] += Math.log(data[n][0]);
sum[1] += data[n][1] * Math.log(data[n][0]);
sum[2] += data[n][1];
sum[3] += (Math.log(data[n][0]) ** 2);
}
}
const a = ((len * sum[1]) - (sum[2] * sum[0])) / ((len * sum[3]) - (sum[0] * sum[0]));
const coeffB = round(a, options.precision);
const coeffA = round((sum[2] - (coeffB * sum[0])) / len, options.precision);
const predict = x => ([
round(x, options.precision),
round(round(coeffA + (coeffB * Math.log(x)), options.precision), options.precision),
]);
const points = data.map(point => predict(point[0]));
return {
points,
predict,
equation: [coeffA, coeffB],
string: `y = ${coeffA} + ${coeffB} ln(x)`,
r2: round(determinationCoefficient(data, points), options.precision),
};
},
power(data, options) {
const sum = [0, 0, 0, 0, 0];
const len = data.length;
for (let n = 0; n < len; n++) {
if (data[n][1] !== null) {
sum[0] += Math.log(data[n][0]);
sum[1] += Math.log(data[n][1]) * Math.log(data[n][0]);
sum[2] += Math.log(data[n][1]);
sum[3] += (Math.log(data[n][0]) ** 2);
}
}
const b = ((len * sum[1]) - (sum[0] * sum[2])) / ((len * sum[3]) - (sum[0] ** 2));
const a = ((sum[2] - (b * sum[0])) / len);
const coeffA = round(Math.exp(a), options.precision);
const coeffB = round(b, options.precision);
const predict = x => ([
round(x, options.precision),
round(round(coeffA * (x ** coeffB), options.precision), options.precision),
]);
const points = data.map(point => predict(point[0]));
return {
points,
predict,
equation: [coeffA, coeffB],
string: `y = ${coeffA}x^${coeffB}`,
r2: round(determinationCoefficient(data, points), options.precision),
};
},
polynomial(data, options) {
const lhs = [];
const rhs = [];
let a = 0;
let b = 0;
const len = data.length;
const k = options.order + 1;
for (let i = 0; i < k; i++) {
for (let l = 0; l < len; l++) {
if (data[l][1] !== null) {
a += (data[l][0] ** i) * data[l][1];
}
}
lhs.push(a);
a = 0;
const c = [];
for (let j = 0; j < k; j++) {
for (let l = 0; l < len; l++) {
if (data[l][1] !== null) {
b += data[l][0] ** (i + j);
}
}
c.push(b);
b = 0;
}
rhs.push(c);
}
rhs.push(lhs);
const coefficients = gaussianElimination(rhs, k).map(v => round(v, options.precision));
const predict = x => ([
round(x, options.precision),
round(
coefficients.reduce((sum, coeff, power) => sum + (coeff * (x ** power)), 0),
options.precision,
),
]);
const points = data.map(point => predict(point[0]));
let string = 'y = ';
for (let i = coefficients.length - 1; i >= 0; i--) {
if (i > 1) {
string += `${coefficients[i]}x^${i} + `;
} else if (i === 1) {
string += `${coefficients[i]}x + `;
} else {
string += coefficients[i];
}
}
return {
string,
points,
predict,
equation: [...coefficients].reverse(),
r2: round(determinationCoefficient(data, points), options.precision),
};
},
};
function createWrapper() {
const reduce = (accumulator, name) => ({
_round: round,
...accumulator,
[name](data, supplied) {
return methods[name](data, {
...DEFAULT_OPTIONS,
...supplied,
});
},
});
return Object.keys(methods).reduce(reduce, {});
}
module.exports = createWrapper();
|