@thi.ng/k-means 0.6.48 → 0.6.49
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.
- package/CHANGELOG.md +1 -1
- package/README.md +1 -1
- package/api.js +0 -1
- package/kmeans.js +115 -172
- package/package.json +11 -8
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
package/api.js
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/kmeans.js
CHANGED
|
@@ -7,186 +7,129 @@ import { add } from "@thi.ng/vectors/add";
|
|
|
7
7
|
import { median } from "@thi.ng/vectors/median";
|
|
8
8
|
import { mulN } from "@thi.ng/vectors/muln";
|
|
9
9
|
import { zeroes } from "@thi.ng/vectors/setn";
|
|
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
|
-
while (update && maxIter-- > 0) {
|
|
38
|
-
update = assign(samples, centroids, clusters, dist);
|
|
39
|
-
if (!update)
|
|
40
|
-
break;
|
|
41
|
-
for (let i = 0; i < k; i++) {
|
|
42
|
-
const impl = strategy(dim);
|
|
43
|
-
for (let j = 0; j < num; j++) {
|
|
44
|
-
i === clusters[j] && impl.update(samples[j]);
|
|
45
|
-
}
|
|
46
|
-
const centroid = impl.finish();
|
|
47
|
-
if (centroid)
|
|
48
|
-
centroids[i] = centroid;
|
|
49
|
-
}
|
|
10
|
+
const kmeans = (k, samples, opts) => {
|
|
11
|
+
let { dist, initial, maxIter, rnd, strategy } = {
|
|
12
|
+
dist: DIST_SQ,
|
|
13
|
+
maxIter: 32,
|
|
14
|
+
strategy: means,
|
|
15
|
+
...opts
|
|
16
|
+
};
|
|
17
|
+
const num = samples.length;
|
|
18
|
+
const dim = samples[0].length;
|
|
19
|
+
const centroidIDs = initial || initKmeanspp(k, samples, dist, rnd);
|
|
20
|
+
assert(centroidIDs.length > 0, `missing initial centroids`);
|
|
21
|
+
k = centroidIDs.length;
|
|
22
|
+
const centroids = centroidIDs.map((i) => samples[i]);
|
|
23
|
+
const clusters = new Uint32Array(num).fill(k);
|
|
24
|
+
let update = true;
|
|
25
|
+
while (update && maxIter-- > 0) {
|
|
26
|
+
update = assign(samples, centroids, clusters, dist);
|
|
27
|
+
if (!update)
|
|
28
|
+
break;
|
|
29
|
+
for (let i = 0; i < k; i++) {
|
|
30
|
+
const impl = strategy(dim);
|
|
31
|
+
for (let j = 0; j < num; j++) {
|
|
32
|
+
i === clusters[j] && impl.update(samples[j]);
|
|
33
|
+
}
|
|
34
|
+
const centroid = impl.finish();
|
|
35
|
+
if (centroid)
|
|
36
|
+
centroids[i] = centroid;
|
|
50
37
|
}
|
|
51
|
-
|
|
38
|
+
}
|
|
39
|
+
return buildClusters(centroids, clusters);
|
|
52
40
|
};
|
|
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
|
-
const centroids = [samples[centroidIDs[0]]];
|
|
79
|
-
const indices = new Array(num).fill(0).map((_, i) => i);
|
|
80
|
-
const metric = dist.metric;
|
|
81
|
-
while (centroidIDs.length < k) {
|
|
82
|
-
let psum = 0;
|
|
83
|
-
const probs = samples.map((p) => {
|
|
84
|
-
const d = dist.from(metric(p, centroids[argmin(p, centroids, dist)])) **
|
|
85
|
-
2;
|
|
86
|
-
psum += d;
|
|
87
|
-
return d;
|
|
88
|
-
});
|
|
89
|
-
if (!psum)
|
|
90
|
-
break;
|
|
91
|
-
let id;
|
|
92
|
-
do {
|
|
93
|
-
id = weightedRandom(indices, probs, rnd)();
|
|
94
|
-
} while (centroidIDs.includes(id));
|
|
95
|
-
centroidIDs.push(id);
|
|
96
|
-
centroids.push(samples[id]);
|
|
97
|
-
}
|
|
98
|
-
return centroidIDs;
|
|
41
|
+
const initKmeanspp = (k, samples, dist = DIST_SQ, rnd = SYSTEM) => {
|
|
42
|
+
const num = samples.length;
|
|
43
|
+
assert(num > 0, `missing samples`);
|
|
44
|
+
k = Math.min(k, num);
|
|
45
|
+
const centroidIDs = [rnd.int() % num];
|
|
46
|
+
const centroids = [samples[centroidIDs[0]]];
|
|
47
|
+
const indices = new Array(num).fill(0).map((_, i) => i);
|
|
48
|
+
const metric = dist.metric;
|
|
49
|
+
while (centroidIDs.length < k) {
|
|
50
|
+
let psum = 0;
|
|
51
|
+
const probs = samples.map((p) => {
|
|
52
|
+
const d = dist.from(metric(p, centroids[argmin(p, centroids, dist)])) ** 2;
|
|
53
|
+
psum += d;
|
|
54
|
+
return d;
|
|
55
|
+
});
|
|
56
|
+
if (!psum)
|
|
57
|
+
break;
|
|
58
|
+
let id;
|
|
59
|
+
do {
|
|
60
|
+
id = weightedRandom(indices, probs, rnd)();
|
|
61
|
+
} while (centroidIDs.includes(id));
|
|
62
|
+
centroidIDs.push(id);
|
|
63
|
+
centroids.push(samples[id]);
|
|
64
|
+
}
|
|
65
|
+
return centroidIDs;
|
|
99
66
|
};
|
|
100
67
|
const assign = (samples, centroids, assignments, dist) => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
68
|
+
let update = false;
|
|
69
|
+
for (let i = samples.length; i-- > 0; ) {
|
|
70
|
+
const id = argmin(samples[i], centroids, dist);
|
|
71
|
+
if (id !== assignments[i]) {
|
|
72
|
+
assignments[i] = id;
|
|
73
|
+
update = true;
|
|
108
74
|
}
|
|
109
|
-
|
|
75
|
+
}
|
|
76
|
+
return update;
|
|
110
77
|
};
|
|
111
78
|
const buildClusters = (centroids, assignments) => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return clusters.filter((x) => !!x);
|
|
79
|
+
const clusters = [];
|
|
80
|
+
for (let i = 0, n = assignments.length; i < n; i++) {
|
|
81
|
+
const id = assignments[i];
|
|
82
|
+
(clusters[id] || (clusters[id] = {
|
|
83
|
+
id,
|
|
84
|
+
centroid: centroids[id],
|
|
85
|
+
items: []
|
|
86
|
+
})).items.push(i);
|
|
87
|
+
}
|
|
88
|
+
return clusters.filter((x) => !!x);
|
|
123
89
|
};
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
update: (p) => {
|
|
135
|
-
add(acc, acc, p);
|
|
136
|
-
n++;
|
|
137
|
-
},
|
|
138
|
-
finish: () => (n ? mulN(acc, acc, 1 / n) : undefined),
|
|
139
|
-
};
|
|
90
|
+
const means = (dim) => {
|
|
91
|
+
const acc = zeroes(dim);
|
|
92
|
+
let n = 0;
|
|
93
|
+
return {
|
|
94
|
+
update: (p) => {
|
|
95
|
+
add(acc, acc, p);
|
|
96
|
+
n++;
|
|
97
|
+
},
|
|
98
|
+
finish: () => n ? mulN(acc, acc, 1 / n) : void 0
|
|
99
|
+
};
|
|
140
100
|
};
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
101
|
+
const medians = () => {
|
|
102
|
+
const acc = [];
|
|
103
|
+
return {
|
|
104
|
+
update: (p) => acc.push(p),
|
|
105
|
+
finish: () => acc.length ? median([], acc) : void 0
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
const meansLatLon = () => {
|
|
109
|
+
let lat = 0;
|
|
110
|
+
let lon = 0;
|
|
111
|
+
let n = 0;
|
|
112
|
+
return {
|
|
113
|
+
update: ([$lat, $lon]) => {
|
|
114
|
+
lat += $lat < 0 ? $lat + 360 : $lat;
|
|
115
|
+
lon += $lon;
|
|
116
|
+
n++;
|
|
117
|
+
},
|
|
118
|
+
finish: () => {
|
|
119
|
+
if (!n)
|
|
120
|
+
return;
|
|
121
|
+
lat /= n;
|
|
122
|
+
if (lat > 180)
|
|
123
|
+
lat -= 360;
|
|
124
|
+
lon /= n;
|
|
125
|
+
return [lat, lon];
|
|
126
|
+
}
|
|
127
|
+
};
|
|
153
128
|
};
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
* @remarks
|
|
161
|
-
* When using this strategy, you should also use the
|
|
162
|
-
* [`HAVERSINE_LATLON`](https://docs.thi.ng/umbrella/distance/variables/HAVERSINE_LATLON.html)
|
|
163
|
-
* distance metric for {@link KMeansOpts.distance}.
|
|
164
|
-
*
|
|
165
|
-
* @example
|
|
166
|
-
* ```ts
|
|
167
|
-
* kmeans(3, [...], { strategy: meansLatLon, dist: HAVERSINE_LATLON })
|
|
168
|
-
* ```
|
|
169
|
-
*
|
|
170
|
-
* https://en.wikipedia.org/wiki/World_Geodetic_System
|
|
171
|
-
*/
|
|
172
|
-
export const meansLatLon = () => {
|
|
173
|
-
let lat = 0;
|
|
174
|
-
let lon = 0;
|
|
175
|
-
let n = 0;
|
|
176
|
-
return {
|
|
177
|
-
update: ([$lat, $lon]) => {
|
|
178
|
-
lat += $lat < 0 ? $lat + 360 : $lat;
|
|
179
|
-
lon += $lon;
|
|
180
|
-
n++;
|
|
181
|
-
},
|
|
182
|
-
finish: () => {
|
|
183
|
-
if (!n)
|
|
184
|
-
return;
|
|
185
|
-
lat /= n;
|
|
186
|
-
if (lat > 180)
|
|
187
|
-
lat -= 360;
|
|
188
|
-
lon /= n;
|
|
189
|
-
return [lat, lon];
|
|
190
|
-
},
|
|
191
|
-
};
|
|
129
|
+
export {
|
|
130
|
+
initKmeanspp,
|
|
131
|
+
kmeans,
|
|
132
|
+
means,
|
|
133
|
+
meansLatLon,
|
|
134
|
+
medians
|
|
192
135
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thi.ng/k-means",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.49",
|
|
4
4
|
"description": "Configurable k-means & k-medians (with k-means++ initialization) for n-D vectors",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./index.js",
|
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
"author": "Karsten Schmidt (https://thi.ng)",
|
|
25
25
|
"license": "Apache-2.0",
|
|
26
26
|
"scripts": {
|
|
27
|
-
"build": "yarn
|
|
27
|
+
"build": "yarn build:esbuild && yarn build:decl",
|
|
28
|
+
"build:decl": "tsc --declaration --emitDeclarationOnly",
|
|
29
|
+
"build:esbuild": "esbuild --format=esm --platform=neutral --target=es2022 --tsconfig=tsconfig.json --outdir=. src/**/*.ts",
|
|
28
30
|
"clean": "rimraf --glob '*.js' '*.d.ts' '*.map' doc",
|
|
29
31
|
"doc": "typedoc --excludePrivate --excludeInternal --out doc src/index.ts",
|
|
30
32
|
"doc:ae": "mkdir -p .ae/doc .ae/temp && api-extractor run --local --verbose",
|
|
@@ -33,14 +35,15 @@
|
|
|
33
35
|
"test": "bun test"
|
|
34
36
|
},
|
|
35
37
|
"dependencies": {
|
|
36
|
-
"@thi.ng/api": "^8.9.
|
|
37
|
-
"@thi.ng/distance": "^2.4.
|
|
38
|
-
"@thi.ng/errors": "^2.4.
|
|
39
|
-
"@thi.ng/random": "^3.6.
|
|
40
|
-
"@thi.ng/vectors": "^7.8.
|
|
38
|
+
"@thi.ng/api": "^8.9.12",
|
|
39
|
+
"@thi.ng/distance": "^2.4.34",
|
|
40
|
+
"@thi.ng/errors": "^2.4.6",
|
|
41
|
+
"@thi.ng/random": "^3.6.18",
|
|
42
|
+
"@thi.ng/vectors": "^7.8.9"
|
|
41
43
|
},
|
|
42
44
|
"devDependencies": {
|
|
43
45
|
"@microsoft/api-extractor": "^7.38.3",
|
|
46
|
+
"esbuild": "^0.19.8",
|
|
44
47
|
"rimraf": "^5.0.5",
|
|
45
48
|
"tools": "^0.0.1",
|
|
46
49
|
"typedoc": "^0.25.4",
|
|
@@ -80,5 +83,5 @@
|
|
|
80
83
|
"status": "beta",
|
|
81
84
|
"year": 2021
|
|
82
85
|
},
|
|
83
|
-
"gitHead": "
|
|
86
|
+
"gitHead": "5e7bafedfc3d53bc131469a28de31dd8e5b4a3ff\n"
|
|
84
87
|
}
|