UNPKG

@ai-on-browser/data-analysis-models

Version:

Data analysis model package without any dependencies

348 lines (302 loc) 8.9 kB
class Point { constructor(p, value = null) { this._p = p this.value = value } get x() { return this._p[0] } get y() { return this._p[1] } } class Circle { constructor(c, r) { this._c = c this._r = r } get center() { return this._c } contains(p) { return (p.x - this._c.x) ** 2 + (p.y - this._c.y) ** 2 < this._r ** 2 } } class Triangle { constructor(p1, p2, p3) { this.p = [p1, p2, p3] this.adjoin = [null, null, null] this._circumcircle = null } get p() { return this._p } set p(points) { this._p = points this._circumcircle = null } get circumcircle() { if (this._circumcircle) { return this._circumcircle } const [p1, p2, p3] = this._p const c = 2 * ((p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x)) + 1.0e-12 const c21 = p2.x ** 2 - p1.x ** 2 + p2.y ** 2 - p1.y ** 2 const c31 = p3.x ** 2 - p1.x ** 2 + p3.y ** 2 - p1.y ** 2 const cx = ((p3.y - p1.y) * c21 + (p1.y - p2.y) * c31) / c const cy = ((p1.x - p3.x) * c21 + (p2.x - p1.x) * c31) / c this._circumcircle = new Circle(new Point([cx, cy]), Math.sqrt((cx - p1.x) ** 2 + (cy - p1.y) ** 2)) return this._circumcircle } contains(p) { const outer = (p1, p2, p3) => { return (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y) } const o = [] for (let i = 0; i < 3; i++) { const oi = outer(p, this.p[i], this.p[(i + 1) % 3]) if (oi === 0) { continue } if (o.length > 0 && o[o.length - 1] !== oi < 0) { return false } o.push(oi < 0) } return true } contains_circle(p) { return this.circumcircle.contains(p) } } class Delaunay2D { constructor(points) { this._points = points const n = this._points.length const min = [Infinity, Infinity] const max = [-Infinity, -Infinity] for (let i = 0; i < n; i++) { for (let d = 0; d < 2; d++) { min[d] = Math.min(min[d], this._points[i]._p[d]) max[d] = Math.max(max[d], this._points[i]._p[d]) } } for (let d = 0; d < 2; d++) { min[d] -= 1 max[d] += 1 } const rootPoints = [ new Point([min[0] - (max[1] - min[1]), min[1]]), new Point([max[0] + (max[1] - min[1]), min[1]]), new Point([(min[0] + max[0]) / 2, max[1] + (max[0] - min[0]) / 2]), ] const triangles = [new Triangle(...rootPoints)] for (let i = 0; i < n; i++) { const xi = this._points[i] let k = 0 for (; k < triangles.length; k++) { if (triangles[k].contains(xi)) { break } } const t = triangles.splice(k, 1)[0] const nt1 = new Triangle(xi, t.p[1], t.p[2]) const nt2 = new Triangle(xi, t.p[2], t.p[0]) const nt3 = new Triangle(xi, t.p[0], t.p[1]) nt1.adjoin = [t.adjoin[0], nt2, nt3] nt2.adjoin = [t.adjoin[1], nt3, nt1] nt3.adjoin = [t.adjoin[2], nt1, nt2] const nt = [nt1, nt2, nt3] for (let j = 0; j < t.adjoin.length; j++) { if (!t.adjoin[j]) { continue } const m = t.adjoin[j].adjoin.indexOf(t) t.adjoin[j].adjoin[m] = nt[j] } triangles.push(...nt) const checkFlip = nt.map(t => [t, 0]) while (checkFlip.length > 0) { const [cf, j] = checkFlip.pop() const ad = cf.adjoin[j] if (!ad) { continue } const m = ad.adjoin.indexOf(cf) if (!cf.contains_circle(ad.p[m])) { continue } const j1 = (j + 1) % 3 const j2 = (j + 2) % 3 let m1 = (m + 1) % 3 let m2 = (m + 2) % 3 if (ad.p[m1].x !== cf.p[j1].x || ad.p[m1].y !== cf.p[j1].y) { ;[m1, m2] = [m2, m1] } const cf_p = cf.p const cf_a = cf.adjoin const ad_a = ad.adjoin cf.p = [cf.p[j], cf.p[j1], ad.p[m]] cf.adjoin = [ad_a[m2], ad, cf_a[j2]] if (ad_a[m2]) { ad_a[m2].adjoin[ad_a[m2].adjoin.indexOf(ad)] = cf } ad.p = [cf_p[j], cf_p[j2], ad.p[m]] ad.adjoin = [ad_a[m1], cf, cf_a[j1]] if (cf_a[j1]) { cf_a[j1].adjoin[cf_a[j1].adjoin.indexOf(cf)] = ad } checkFlip.push([cf, 0]) checkFlip.push([ad, 0]) } } for (let i = triangles.length - 1; i >= 0; i--) { if (triangles[i].p.some(p => rootPoints.some(rp => p.x === rp.x && p.y === rp.y))) { triangles.splice(i, 1) } } this._triangles = triangles } } /** * Natural neighbor interpolation */ export default class NaturalNeighborInterpolation { // https://en.wikipedia.org/wiki/Natural_neighbor_interpolation // https://www.researchgate.net/profile/Christopher-Gold-3/publication/227220827_An_Efficient_Natural_Neighbour_Interpolation_Algorithm_for_Geoscientific_Modelling/links/0c96051bc609cee8b5000000/An-Efficient-Natural-Neighbour-Interpolation-Algorithm-for-Geoscientific-Modelling.pdf // https://gwlucastrig.github.io/TinfourDocs/NaturalNeighborTinfourAlgorithm/index.html /** * Fit model. * @param {Array<Array<number>>} x Training data * @param {number[]} y Target values */ fit(x, y) { if (x[0].length === 1) { const d = x.map((v, i) => [v[0], y[i]]) d.sort((a, b) => a[0] - b[0]) this._x = d.map(v => v[0]) this._y = d.map(v => v[1]) } else if (x[0].length === 2) { const points = x.map((v, i) => new Point(v, y[i])) this._default_delaunay = new Delaunay2D(points) } } /** * Returns probabilities of the datas. * @param {Array<Array<number>>} x Sample data * @returns {number[]} Predicted values */ predict(x) { if (x[0].length === 1) { return x.map(v => { const n = this._x.length if (v[0] <= this._x[0]) { return this._y[0] } else if (v[0] >= this._x[n - 1]) { return this._y[n - 1] } let i = 1 for (; i < n && v[0] > this._x[i]; i++); if (v[0] === this._x[i]) { return this._y[i] } const mid = (this._x[i - 1] + this._x[i]) / 2 const mid0 = (this._x[i - 1] + v[0]) / 2 const mid1 = (v[0] + this._x[i]) / 2 const w0 = mid - mid0 const w1 = mid1 - mid return (w0 * this._y[i - 1] + w1 * this._y[i]) / (w0 + w1) }) } else if (x[0].length === 2) { return x.map(v => { const xp = new Point(v) for (const point of this._default_delaunay._points) { if (xp.x === point.x && xp.y === point.y) { return point.value } } const dp = new Set() for (const triangle of this._default_delaunay._triangles) { if (triangle.contains_circle(xp)) { for (let i = 0; i < triangle.p.length; i++) { dp.add(triangle.p[i]) } } } if (dp.size === 0) { return null } const newDelaunay = new Delaunay2D([xp, ...dp]) const newTriangles = newDelaunay._triangles.filter(t => t.p.includes(xp)) const neighborPoints = new Set() for (const triangle of newTriangles) { for (const point of triangle.p) { neighborPoints.add(point) } } neighborPoints.delete(xp) for (const point of neighborPoints) { if (newTriangles.filter(t => t.p.includes(point)).length < 2) { return null } } const triangles = this._default_delaunay._triangles.filter(t => { return t.p.every(p => neighborPoints.has(p)) }) const w = [] for (const point of neighborPoints) { const newPTri = newTriangles.filter(t => t.p.includes(point)) const adPoints = [] const m = [] for (let i = 0; i < newPTri.length; i++) { for (let k = 0; k < newPTri[i].p.length; k++) { if (newPTri[i].p[k] !== xp && newPTri[i].p[k] !== point) { adPoints[i] = newPTri[i].p[k] break } } if (!newPTri[i].contains(newPTri[i].circumcircle.center)) { m[i] = newPTri[i].circumcircle.center } else { m[i] = new Point([(adPoints[i].x + point.x) / 2, (adPoints[i].y + point.y) / 2]) } } const basePath = [m[1], point, m[0]] const base = basePath.slice(1).reduce((s, p, i) => s + basePath[i].x * p.y - p.x * basePath[i].y, 0) / 2 const g = newPTri.map(t => t.circumcircle.center) const newPath = [m[0], g[0], g[1], m[1]] const a = newPath.slice(1).reduce((s, p, i) => s + newPath[i].x * p.y - p.x * newPath[i].y, 0) / 2 const pTri = triangles.filter(t => t.p.includes(point)) const c = [] let oPoint = adPoints[0] while (pTri.length > 0) { let i = 0 for (; i < pTri.length; i++) { if (pTri[i].p.includes(oPoint)) { break } } const tri = pTri.splice(i, 1)[0] if (!tri) { break } c.push(tri.circumcircle.center) for (let k = 0; k < tri.p.length; k++) { if (tri.p[k] !== oPoint && tri.p[k] !== point) { oPoint = tri.p[k] break } } } const oldPath = [m[0], ...c, m[1]] const t = oldPath.slice(1).reduce((s, p, i) => s + oldPath[i].x * p.y - p.x * oldPath[i].y, 0) / 2 w.push([Math.abs(t + base) - Math.abs(a + base), point.value]) } const wsum = w.reduce((s, v) => s + v[0], 0) return w.reduce((s, v) => s + v[0] * v[1], 0) / wsum }) } throw new Error('Invalid data dimension.') } }