w-cluster 1.0.19 → 1.0.21
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/.github/workflows/ci-test.yml +3 -3
- package/README.md +4 -6
- package/babel.config.js +4 -3
- package/dist/w-cluster.umd.js +2 -2
- package/dist/w-cluster.umd.js.map +1 -1
- package/dist/w-cluster.wk.umd.js +1 -1
- package/docs/DenseMatrix.html +202 -0
- package/docs/DistancePair.html +202 -0
- package/docs/LowerTriangle.html +202 -0
- package/docs/Rec.html +202 -0
- package/docs/Reco.html +202 -0
- package/docs/WCluster.mjs.html +8 -4
- package/docs/examples/ex-PCA.html +1 -1
- package/docs/examples/ex-cluster-webworker.html +1 -1
- package/docs/examples/ex-cluster.html +1 -1
- package/docs/global.html +6799 -164
- package/docs/index.html +2 -2
- package/docs/k-medoids_alternating.mjs.html +144 -0
- package/docs/k-medoids_arrayadapter.mjs.html +107 -0
- package/docs/k-medoids_dynmsc.mjs.html +255 -0
- package/docs/k-medoids_fastermsc.mjs.html +411 -0
- package/docs/k-medoids_fasterpam.mjs.html +305 -0
- package/docs/k-medoids_fastmsc.mjs.html +166 -0
- package/docs/k-medoids_fastpam1.mjs.html +132 -0
- package/docs/k-medoids_initialization.mjs.html +107 -0
- package/docs/k-medoids_pam.mjs.html +275 -0
- package/docs/k-medoids_pammedsil.mjs.html +307 -0
- package/docs/k-medoids_pamsil.mjs.html +225 -0
- package/docs/k-medoids_par_fasterpam.mjs.html +134 -0
- package/docs/k-medoids_par_silhouette.mjs.html +126 -0
- package/docs/k-medoids_silhouette.mjs.html +193 -0
- package/docs/k-medoids_util.mjs.html +135 -0
- package/g-PCA-nodeworker.mjs +1 -1
- package/g-PCA.mjs +1 -1
- package/g-cluster-kMeans-large.mjs +83 -0
- package/g-cluster-kMeans-nodeworker.mjs +181 -0
- package/g-cluster-kMeans.mjs +178 -0
- package/g-cluster-kMedoids-large-suggest.mjs +95 -0
- package/g-cluster-kMedoids-large.mjs +104 -0
- package/{g-cluster-nodeworker.mjs → g-cluster-kMedoids-nodeworker.mjs} +1 -2
- package/{g-cluster.mjs → g-cluster-kMedoids-simple.mjs} +1 -1
- package/package.json +7 -7
- package/script.txt +18 -0
- package/src/WCluster.mjs +6 -2
- package/src/WClusterCore.mjs +30 -21
- package/src/WClusterMat.mjs +98 -24
- package/src/WPCAMat.mjs +2 -2
- package/src/jaccardBitset.mjs +63 -0
- package/src/k-medoids/README.md +124 -0
- package/src/k-medoids/alternating.mjs +72 -0
- package/src/k-medoids/arrayadapter.mjs +35 -0
- package/src/k-medoids/dynmsc.mjs +183 -0
- package/src/k-medoids/fastermsc.mjs +339 -0
- package/src/k-medoids/fasterpam.mjs +233 -0
- package/src/k-medoids/fastmsc.mjs +94 -0
- package/src/k-medoids/fastpam1.mjs +60 -0
- package/src/k-medoids/index.mjs +37 -0
- package/src/k-medoids/initialization.mjs +35 -0
- package/src/k-medoids/package.json +8 -0
- package/src/k-medoids/pam.mjs +203 -0
- package/src/k-medoids/pammedsil.mjs +235 -0
- package/src/k-medoids/pamsil.mjs +153 -0
- package/src/k-medoids/par_fasterpam.mjs +62 -0
- package/src/k-medoids/par_silhouette.mjs +54 -0
- package/src/k-medoids/silhouette.mjs +121 -0
- package/src/k-medoids/test.mjs +357 -0
- package/src/k-medoids/util.mjs +63 -0
- package/test/jaccardBitset.test.mjs +141 -0
- package/test/kMeans.test.mjs +278 -0
- package/test/kMedoids-large-suggest.test.mjs +140 -0
- package/test/kMedoids-large.test.mjs +142 -0
- package/test/{cluster.test.mjs → kMedoids-simple.test.mjs} +5 -5
- package/toolg/gDocsExams.mjs +1 -1
package/script.txt
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#node toolg/addVersion.mjs
|
|
2
|
+
node toolg/modifyReadme.mjs
|
|
3
|
+
|
|
4
|
+
node toolg/cleanFolder.mjs
|
|
5
|
+
./node_modules/.bin/jsdoc -c .jsdoc
|
|
6
|
+
node toolg/gDocsExams.mjs
|
|
7
|
+
|
|
8
|
+
node toolg/gDistRollup.mjs
|
|
9
|
+
|
|
10
|
+
git add . -A
|
|
11
|
+
git commit -m 'modify: '
|
|
12
|
+
git push origin master:master
|
|
13
|
+
|
|
14
|
+
npm run deploy
|
|
15
|
+
|
|
16
|
+
#npm test
|
|
17
|
+
|
|
18
|
+
#npm publish
|
package/src/WCluster.mjs
CHANGED
|
@@ -25,12 +25,16 @@ async function PCA(data, opt = {}) {
|
|
|
25
25
|
* @param {Number} [opt.kNumber=2] 輸入指定分群數整數,不能超過數據的長度,預設2
|
|
26
26
|
* @param {Number} [opt.nCompNIPALS=2] 輸入指定降維的維度整數,不能超過數據的維度(各列有效元素數量),預設2
|
|
27
27
|
* @param {String} [opt.mode='k-medoids'] 輸入分群方法字串,可為'k-means'、'k-medoids',k-means受初始隨機群中心影響較大,預設'k-medoids'
|
|
28
|
-
* @
|
|
28
|
+
* @param {Boolean} [opt.usePCA=true] 輸入分群前是否先對數據做PCA降維布林值,預設true;若使用自訂非歐距離(如Jaccard對二元向量)應設為false,以保留原始數據語意
|
|
29
|
+
* @param {Function} [opt.funDist] 輸入自訂距離函數,函數接收兩數據陣列(t1,t2)並回傳兩者距離數值,僅於mode為'k-medoids'時生效,未提供時底層採用歐氏(euclidean)距離
|
|
30
|
+
* @param {String} [opt.useMethod='fasterPAM'] 輸入k-medoids分群後端字串,僅於mode為'k-medoids'時生效,'simple'為npm套件'k-medoids'(樸素PAM),'fasterPAM'為內建./src/k-medoids之矩陣式FasterPAM(大數據較快),預設'fasterPAM'
|
|
31
|
+
* @param {Number} [opt.seed] 輸入k-means隨機初始化種子整數,僅於mode為'k-means'時生效,給定則分群結果可重現,未提供時為隨機初始化
|
|
32
|
+
* @return {Promise} 回傳Promise,resolve回傳分群後結果物件,keys代表分群有使用到的欄位,ginds代表分群後的指標陣列,gltdt代表分群後的物件陣列,gmat代表分群後的數據陣列(usePCA為true時為PCA降維後數據,可例如取最左2欄代表x,y繪製二維分佈圖;usePCA為false時為原始數據),reject回傳錯誤訊息
|
|
29
33
|
* @example
|
|
30
34
|
*/
|
|
31
35
|
async function cluster(data, opt = {}) {
|
|
32
36
|
try {
|
|
33
|
-
return WClusterCore(data, opt)
|
|
37
|
+
return await WClusterCore(data, opt)
|
|
34
38
|
}
|
|
35
39
|
catch (err) {
|
|
36
40
|
return Promise.reject(err)
|
package/src/WClusterCore.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import get from 'lodash-es/get'
|
|
2
|
-
import each from 'lodash-es/each'
|
|
3
|
-
import size from 'lodash-es/size'
|
|
4
|
-
|
|
5
|
-
import
|
|
6
|
-
import values from 'lodash-es/values'
|
|
1
|
+
import get from 'lodash-es/get.js'
|
|
2
|
+
import each from 'lodash-es/each.js'
|
|
3
|
+
import size from 'lodash-es/size.js'
|
|
4
|
+
import map from 'lodash-es/map.js'
|
|
5
|
+
import values from 'lodash-es/values.js'
|
|
7
6
|
import dtpick from 'wsemi/src/dtpick.mjs'
|
|
7
|
+
import isbol from 'wsemi/src/isbol.mjs'
|
|
8
8
|
import isnum from 'wsemi/src/isnum.mjs'
|
|
9
9
|
import isearr from 'wsemi/src/isearr.mjs'
|
|
10
10
|
import iseobj from 'wsemi/src/iseobj.mjs'
|
|
@@ -48,7 +48,7 @@ function getEffData(ltdt, ks) {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
|
|
51
|
-
function WClusterCore(data, opt = {}) {
|
|
51
|
+
async function WClusterCore(data, opt = {}) {
|
|
52
52
|
|
|
53
53
|
//check
|
|
54
54
|
if (!isearr(data)) {
|
|
@@ -56,14 +56,20 @@ function WClusterCore(data, opt = {}) {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
//kNumber
|
|
59
|
-
let kNumber = get(opt, 'kNumber')
|
|
59
|
+
let kNumber = get(opt, 'kNumber', null)
|
|
60
60
|
if (!isnum(kNumber)) {
|
|
61
61
|
kNumber = 2
|
|
62
62
|
}
|
|
63
63
|
kNumber = cint(kNumber)
|
|
64
64
|
|
|
65
|
+
//usePCA
|
|
66
|
+
let usePCA = get(opt, 'usePCA', null)
|
|
67
|
+
if (!isbol(usePCA)) {
|
|
68
|
+
usePCA = true
|
|
69
|
+
}
|
|
70
|
+
|
|
65
71
|
//nCompNIPALS
|
|
66
|
-
let nCompNIPALS = get(opt, 'nCompNIPALS')
|
|
72
|
+
let nCompNIPALS = get(opt, 'nCompNIPALS', null)
|
|
67
73
|
if (!isnum(nCompNIPALS)) {
|
|
68
74
|
nCompNIPALS = 2
|
|
69
75
|
}
|
|
@@ -71,7 +77,16 @@ function WClusterCore(data, opt = {}) {
|
|
|
71
77
|
// console.log('nCompNIPALS', nCompNIPALS)
|
|
72
78
|
|
|
73
79
|
//mode, 由WDataCluster檢核與給予預設值
|
|
74
|
-
let mode = get(opt, 'mode')
|
|
80
|
+
let mode = get(opt, 'mode', null)
|
|
81
|
+
|
|
82
|
+
//seed
|
|
83
|
+
let seed = get(opt, 'seed', null)
|
|
84
|
+
|
|
85
|
+
//funDist
|
|
86
|
+
let funDist = get(opt, 'funDist', null)
|
|
87
|
+
|
|
88
|
+
//useMethod
|
|
89
|
+
let useMethod = get(opt, 'useMethod', 'fasterPAM')
|
|
75
90
|
|
|
76
91
|
let type = ''
|
|
77
92
|
let n = size(data)
|
|
@@ -87,11 +102,9 @@ function WClusterCore(data, opt = {}) {
|
|
|
87
102
|
})
|
|
88
103
|
if (iobj === n) {
|
|
89
104
|
type = 'obj'
|
|
90
|
-
// return WClusterCore(data, opt)
|
|
91
105
|
}
|
|
92
106
|
else if (imat === n) {
|
|
93
107
|
type = 'mat'
|
|
94
|
-
// return WClusterMat(data, opt)
|
|
95
108
|
}
|
|
96
109
|
else {
|
|
97
110
|
throw new Error('data is not of the same type')
|
|
@@ -113,17 +126,13 @@ function WClusterCore(data, opt = {}) {
|
|
|
113
126
|
}
|
|
114
127
|
|
|
115
128
|
//WPCAMat
|
|
116
|
-
let pcad =
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// v = take(v, nCompNIPALS)
|
|
121
|
-
// return v
|
|
122
|
-
// })
|
|
123
|
-
// // console.log('pcad', pcad)
|
|
129
|
+
let pcad = mat
|
|
130
|
+
if (usePCA) {
|
|
131
|
+
pcad = WPCAMat(mat, { nCompNIPALS })
|
|
132
|
+
}
|
|
124
133
|
|
|
125
134
|
//WClusterMat
|
|
126
|
-
let ginds = WClusterMat(pcad, { kNumber, mode })
|
|
135
|
+
let ginds = await WClusterMat(pcad, { kNumber, mode, seed, funDist, useMethod })
|
|
127
136
|
|
|
128
137
|
//gmat
|
|
129
138
|
let gmat = map(ginds, (inds) => {
|
package/src/WClusterMat.mjs
CHANGED
|
@@ -1,38 +1,76 @@
|
|
|
1
|
-
import get from 'lodash-es/get'
|
|
2
|
-
import each from 'lodash-es/each'
|
|
3
|
-
import map from 'lodash-es/map'
|
|
4
|
-
import
|
|
5
|
-
import values from 'lodash-es/values'
|
|
1
|
+
import get from 'lodash-es/get.js'
|
|
2
|
+
import each from 'lodash-es/each.js'
|
|
3
|
+
import map from 'lodash-es/map.js'
|
|
4
|
+
import values from 'lodash-es/values.js'
|
|
6
5
|
import isnum from 'wsemi/src/isnum.mjs'
|
|
6
|
+
import isfun from 'wsemi/src/isfun.mjs'
|
|
7
7
|
import cint from 'wsemi/src/cint.mjs'
|
|
8
8
|
import kmeds from 'k-medoids'
|
|
9
9
|
import { kmeans } from 'ml-kmeans'
|
|
10
|
+
import { fasterpam, first_k } from './k-medoids/index.mjs'
|
|
10
11
|
|
|
11
12
|
|
|
12
|
-
function
|
|
13
|
+
function euclidean(a, b) {
|
|
14
|
+
//歐氏距離
|
|
15
|
+
let s = 0
|
|
16
|
+
for (let i = 0; i < a.length; i++) {
|
|
17
|
+
let d = a[i] - b[i]
|
|
18
|
+
s += d * d
|
|
19
|
+
}
|
|
20
|
+
return Math.sqrt(s)
|
|
21
|
+
}
|
|
13
22
|
|
|
14
|
-
//kp
|
|
15
|
-
let kp = {}
|
|
16
|
-
each(data, (v, k) => {
|
|
17
|
-
let key = getKey(v)
|
|
18
|
-
kp[key] = k
|
|
19
|
-
})
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
function kMedoidsFasterPAM(data, kNumber, funDist) {
|
|
25
|
+
//fasterPAM後端, 先以funDist(或預設歐氏)建一次 n×n 距離矩陣(每對只算一次), 再跑fasterpam, 把結果轉回各群原始index
|
|
26
|
+
|
|
27
|
+
let n = data.length
|
|
28
|
+
let dist = isfun(funDist) ? funDist : euclidean
|
|
29
|
+
|
|
30
|
+
//建 n×n 對稱距離矩陣
|
|
31
|
+
let D = Array.from({ length: n }, () => new Float64Array(n))
|
|
32
|
+
for (let i = 0; i < n; i++) {
|
|
33
|
+
for (let j = i + 1; j < n; j++) {
|
|
34
|
+
let d = dist(data[i], data[j])
|
|
35
|
+
D[i][j] = d
|
|
36
|
+
D[j][i] = d
|
|
37
|
+
}
|
|
25
38
|
}
|
|
26
39
|
|
|
40
|
+
//fasterpam: med為初始medoid(取前kNumber個, 就地更新), 回傳assi=各點所屬medoid slot
|
|
41
|
+
let med = first_k(kNumber)
|
|
42
|
+
let r = fasterpam(D, med, 100)
|
|
43
|
+
|
|
44
|
+
//依slot把點分群 → 各群原始index
|
|
45
|
+
let gs = {}
|
|
46
|
+
each(r.assi, (slot, i) => {
|
|
47
|
+
if (!gs[slot]) {
|
|
48
|
+
gs[slot] = []
|
|
49
|
+
}
|
|
50
|
+
gs[slot].push(i)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
return values(gs)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
function kMedoidsSimple(data, kNumber, funDist) {
|
|
58
|
+
|
|
59
|
+
//kp, 以「元素參照」對應原始index
|
|
60
|
+
// getClusteredData回傳原元素參照, 故可用參照反查; 同時天然避免「值相同列」碰撞
|
|
61
|
+
let kp = new Map()
|
|
62
|
+
each(data, (v, k) => {
|
|
63
|
+
kp.set(v, k)
|
|
64
|
+
})
|
|
65
|
+
|
|
27
66
|
//getInstance
|
|
28
|
-
let intance = kmeds.Clusterer.getInstance(data, kNumber)
|
|
67
|
+
let intance = kmeds.Clusterer.getInstance(data, kNumber, funDist)
|
|
29
68
|
|
|
30
69
|
//cluster
|
|
31
70
|
let rs = intance.getClusteredData()
|
|
32
71
|
rs = map(rs, (gs) => {
|
|
33
72
|
return map(gs, (v) => {
|
|
34
|
-
let
|
|
35
|
-
let ind = kp[key]
|
|
73
|
+
let ind = kp.get(v)
|
|
36
74
|
return ind
|
|
37
75
|
})
|
|
38
76
|
})
|
|
@@ -41,10 +79,34 @@ function kMedoids(data, kNumber) {
|
|
|
41
79
|
}
|
|
42
80
|
|
|
43
81
|
|
|
44
|
-
function
|
|
82
|
+
async function kMedoids(data, kNumber, opt = {}) {
|
|
83
|
+
|
|
84
|
+
//funDist
|
|
85
|
+
let funDist = get(opt, 'funDist', null)
|
|
86
|
+
|
|
87
|
+
//useMethod, 選擇k-medoids後端: 'simple'=npm套件'k-medoids'(樸素PAM); 其他(預設'fasterPAM')=./src/k-medoids矩陣式FasterPAM
|
|
88
|
+
let useMethod = get(opt, 'useMethod', 'fasterPAM')
|
|
89
|
+
if (useMethod === 'simple') {
|
|
90
|
+
return kMedoidsSimple(data, kNumber, funDist)
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
return kMedoidsFasterPAM(data, kNumber, funDist)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async function kMeans(data, kNumber, opt = {}) {
|
|
100
|
+
|
|
101
|
+
//seed
|
|
102
|
+
let seed = get(opt, 'seed', null)
|
|
45
103
|
|
|
46
104
|
//kmeans
|
|
47
|
-
let
|
|
105
|
+
let optKMeans = {}
|
|
106
|
+
if (seed !== null) {
|
|
107
|
+
optKMeans = { seed }
|
|
108
|
+
}
|
|
109
|
+
let r = kmeans(data, kNumber, optKMeans)
|
|
48
110
|
//k: [ 0, 1, 2, 3 ]
|
|
49
111
|
//clusters: [ 0, 0, 1, 0 ]
|
|
50
112
|
|
|
@@ -68,8 +130,11 @@ function kMeans(data, kNumber) {
|
|
|
68
130
|
// @param {Object} [opt={}] 輸入設定物件,預設{}
|
|
69
131
|
// @param {Number} [opt.kNumber=2] 輸入指定分群數整數,不能超過數據的長度,預設2
|
|
70
132
|
// @param {String} [opt.mode='k-medoids'] 輸入分群方法字串,可為'k-means'、'k-medoids',k-means受初始隨機群中心影響較大,預設'k-medoids'
|
|
133
|
+
// @param {Function} [opt.funDist] 輸入自訂距離函數,函數接收兩數據陣列(t1,t2)並回傳兩者距離數值,僅於mode為'k-medoids'時生效,未提供時底層採用歐氏(euclidean)距離
|
|
134
|
+
// @param {String} [opt.useMethod='fasterPAM'] 輸入k-medoids分群後端字串,僅於mode為'k-medoids'時生效,'simple'為npm套件'k-medoids'(樸素PAM),'fasterPAM'為內建./src/k-medoids之矩陣式FasterPAM(大數據較快),預設'fasterPAM'
|
|
135
|
+
// @param {Number} [opt.seed] 輸入k-means隨機初始化種子整數,僅於mode為'k-means'時生效,給定則分群結果可重現,未提供時為隨機初始化
|
|
71
136
|
// @return {Array} 回傳分群後各群所屬items的指標陣列
|
|
72
|
-
function WClusterMat(data, opt = {}) {
|
|
137
|
+
async function WClusterMat(data, opt = {}) {
|
|
73
138
|
|
|
74
139
|
//kNumber
|
|
75
140
|
let kNumber = get(opt, 'kNumber')
|
|
@@ -86,12 +151,21 @@ function WClusterMat(data, opt = {}) {
|
|
|
86
151
|
mode = 'k-medoids'
|
|
87
152
|
}
|
|
88
153
|
|
|
154
|
+
//seed
|
|
155
|
+
let seed = get(opt, 'seed', null)
|
|
156
|
+
|
|
157
|
+
//funDist
|
|
158
|
+
let funDist = get(opt, 'funDist', null)
|
|
159
|
+
|
|
160
|
+
//useMethod
|
|
161
|
+
let useMethod = get(opt, 'useMethod', 'fasterPAM')
|
|
162
|
+
|
|
89
163
|
let rs
|
|
90
164
|
if (mode === 'k-medoids') {
|
|
91
|
-
rs = kMedoids(data, kNumber)
|
|
165
|
+
rs = await kMedoids(data, kNumber, { funDist, useMethod })
|
|
92
166
|
}
|
|
93
167
|
else if (mode === 'k-means') {
|
|
94
|
-
rs = kMeans(data, kNumber)
|
|
168
|
+
rs = await kMeans(data, kNumber, { seed })
|
|
95
169
|
}
|
|
96
170
|
else {
|
|
97
171
|
throw new Error(`invalid mode[${mode}]`)
|
package/src/WPCAMat.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
|
-
import get from 'lodash-es/get'
|
|
3
|
-
import size from 'lodash-es/size'
|
|
2
|
+
import get from 'lodash-es/get.js'
|
|
3
|
+
import size from 'lodash-es/size.js'
|
|
4
4
|
import isearr from 'wsemi/src/isearr.mjs'
|
|
5
5
|
import isbol from 'wsemi/src/isbol.mjs'
|
|
6
6
|
import isnum from 'wsemi/src/isnum.mjs'
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Jaccard 距離的位元打包(bitset) + popcount 加速版。
|
|
2
|
+
//
|
|
3
|
+
// 對「0/1 二元向量」做 k-medoids + Jaccard 分群時, 建距離矩陣為 O(n²·D)。逐維計算
|
|
4
|
+
// Jaccard 在高維很慢; 改用「把 D 個 0/1 位元打包成 ceil(D/32) 個 32-bit word, 以
|
|
5
|
+
// popcount 一次處理 32 維」, 每對距離計算可加速約一個數量級(實測高維 ~14–23x)。
|
|
6
|
+
//
|
|
7
|
+
// 用法:
|
|
8
|
+
// import { jaccardBitset } from './jaccardBitset.mjs'
|
|
9
|
+
// let { data, funDist } = jaccardBitset(rows) //rows: 各列為等長 0/1 陣列
|
|
10
|
+
// let rc = await WCluster.cluster(data, { mode: 'k-medoids', kNumber, usePCA: false, funDist, useMethod: 'fasterPAM' })
|
|
11
|
+
// //rc.ginds 為各群所屬「原始列 index」, 與 rows 對應
|
|
12
|
+
//
|
|
13
|
+
// 注意:
|
|
14
|
+
// 1) 必須 usePCA:false —— 打包後的列是 word 值, 不可再做 PCA。
|
|
15
|
+
// 2) funDist 必須搭配「打包後的 data」使用(兩者由本函數成對產出)。
|
|
16
|
+
// 3) 兩列皆全 0(聯集為 0)時 Jaccard 距離視為相同(0), 與逐維版語意一致。
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
//把單一 0/1 陣列打包成 plain Array(每元素為一個 32-bit word)。
|
|
20
|
+
// 用 plain Array(非 Uint32Array)以相容 WCluster 的陣列型別檢核(isearr 只認真正 Array)。
|
|
21
|
+
function packBits(v) {
|
|
22
|
+
let nw = Math.ceil(v.length / 32)
|
|
23
|
+
let w = new Array(nw).fill(0)
|
|
24
|
+
for (let i = 0; i < v.length; i++) {
|
|
25
|
+
if (v[i]) {
|
|
26
|
+
w[i >> 5] |= (1 << (i & 31))
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return w
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
//32-bit popcount(SWAR); 以 >>> 確保以無號位元樣式計算。
|
|
34
|
+
function popcount32(x) {
|
|
35
|
+
x = x >>> 0
|
|
36
|
+
x = x - ((x >>> 1) & 0x55555555)
|
|
37
|
+
x = (x & 0x33333333) + ((x >>> 2) & 0x33333333)
|
|
38
|
+
x = (x + (x >>> 4)) & 0x0f0f0f0f
|
|
39
|
+
return Math.imul(x, 0x01010101) >>> 24
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
//兩個「打包後列」的 Jaccard 距離 = 1 − |交集| / |聯集|; 聯集為 0(兩列皆空)視為相同(0)。
|
|
44
|
+
function jaccardBits(wa, wb) {
|
|
45
|
+
let inter = 0
|
|
46
|
+
let uni = 0
|
|
47
|
+
for (let i = 0; i < wa.length; i++) {
|
|
48
|
+
inter += popcount32(wa[i] & wb[i])
|
|
49
|
+
uni += popcount32(wa[i] | wb[i])
|
|
50
|
+
}
|
|
51
|
+
return uni === 0 ? 0 : 1 - inter / uni
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
//便利函數: 輸入 0/1 列陣列, 回傳 { data:打包後列陣列, funDist:jaccardBits }, 直接餵 WCluster.cluster。
|
|
56
|
+
function jaccardBitset(rows) {
|
|
57
|
+
let data = rows.map((v) => packBits(v))
|
|
58
|
+
return { data, funDist: jaccardBits }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
export { packBits, popcount32, jaccardBits, jaccardBitset }
|
|
63
|
+
export default jaccardBitset
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# k-medoids — k-Medoids Clustering in JavaScript (ES Modules)
|
|
2
|
+
|
|
3
|
+
A faithful ES module port of [kno10/rust-kmedoids](https://github.com/kno10/rust-kmedoids) to JavaScript.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Node.js >= 18
|
|
8
|
+
- ES modules (`import`/`export`); all files use the `.mjs` extension.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
import { fasterpam, random_initialization, silhouette, LowerTriangle } from './index.mjs';
|
|
14
|
+
|
|
15
|
+
const diss = [[0,1,2,3],[1,0,4,5],[2,4,0,6],[3,5,6,0]];
|
|
16
|
+
const meds = random_initialization(4, 2);
|
|
17
|
+
const { loss, assi, nIter, nSwaps } = fasterpam(diss, meds, 100);
|
|
18
|
+
console.log('Loss:', loss);
|
|
19
|
+
|
|
20
|
+
const { sil } = silhouette(diss, assi, false);
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The `meds` array is mutated in place by all clustering functions (the final medoid indices are written back).
|
|
24
|
+
|
|
25
|
+
## Input format
|
|
26
|
+
|
|
27
|
+
Pass a pairwise dissimilarity matrix as:
|
|
28
|
+
|
|
29
|
+
- A **2D array** `diss[i][j]` (full square matrix), or
|
|
30
|
+
- A **`LowerTriangle`** instance (serialized lower-triangular data, see `arrayadapter.mjs`).
|
|
31
|
+
|
|
32
|
+
All distance values are plain JS `number` (IEEE-754 double).
|
|
33
|
+
|
|
34
|
+
## Return values
|
|
35
|
+
|
|
36
|
+
Public clustering functions return plain objects:
|
|
37
|
+
|
|
38
|
+
| Function(s) | Return shape |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `fasterpam`, `rand_fasterpam`, `fastpam1`, `pam_swap`, `fastmsc`, `fastermsc`, `pamsil_swap`, `pammedsil_swap`, `par_fasterpam` | `{ loss, assi, nIter, nSwaps }` |
|
|
41
|
+
| `alternating` | `{ loss, assi, nIter }` |
|
|
42
|
+
| `pam` | `{ loss, assi, meds, nIter, nSwaps }` |
|
|
43
|
+
| `pam_build` | `{ loss, assi, meds }` |
|
|
44
|
+
| `pamsil`, `pammedsil` | `{ loss, assi, meds, nIter, nSwaps }` |
|
|
45
|
+
| `dynmsc` | `{ loss, assi, nIter, nSwaps, meds, losses }` |
|
|
46
|
+
| `silhouette`, `medoid_silhouette` | `{ sil, samples }` (`samples` is `[]` when `samples=false`) |
|
|
47
|
+
| `par_silhouette` | a plain `number` |
|
|
48
|
+
|
|
49
|
+
## Implemented algorithms
|
|
50
|
+
|
|
51
|
+
- **FasterPAM** (Schubert and Rousseeuw, 2020, 2021) — `fasterpam`
|
|
52
|
+
- **FasterPAM with shuffling** — `rand_fasterpam`
|
|
53
|
+
- **Parallelized FasterPAM** (sequential equivalent in JS) — `par_fasterpam`
|
|
54
|
+
- **FastPAM1** (Schubert and Rousseeuw, 2019, 2021) — `fastpam1`
|
|
55
|
+
- **PAM** (Kaufman and Rousseeuw, 1987) with BUILD and SWAP — `pam`, `pam_build`, `pam_swap`
|
|
56
|
+
- **Alternating optimization** (k-means-style) — `alternating`
|
|
57
|
+
- **FasterMSC** (Lenssen and Schubert, 2022) — `fastermsc`
|
|
58
|
+
- **FastMSC** (Lenssen and Schubert, 2022) — `fastmsc`
|
|
59
|
+
- **DynMSC** (Lenssen and Schubert, 2023) — `dynmsc`
|
|
60
|
+
- **PAMSIL** (Van der Laan and Pollard, 2003) — `pamsil`, `pamsil_swap`
|
|
61
|
+
- **PAMMEDSIL** (Van der Laan and Pollard, 2003) — `pammedsil`, `pammedsil_swap`
|
|
62
|
+
- **Silhouette index** (Rousseeuw, 1987) — `silhouette`, `medoid_silhouette`, `par_silhouette`
|
|
63
|
+
|
|
64
|
+
Note: the k-means-like alternating algorithm tends to find much worse solutions than PAM-based methods.
|
|
65
|
+
|
|
66
|
+
The parallel variants (`par_fasterpam`, `par_silhouette`) are sequential equivalents in this JS port (JS is single-threaded). They produce identical results to the non-parallel variants.
|
|
67
|
+
|
|
68
|
+
## Random initialization
|
|
69
|
+
|
|
70
|
+
`random_initialization(n, k)` returns a length-`k` array of distinct random indices in `[0, n)`.
|
|
71
|
+
Because JS uses a different PRNG than the Rust crate, seeded results will differ numerically from the Rust version even on the same data.
|
|
72
|
+
|
|
73
|
+
## Notes on numerical fidelity
|
|
74
|
+
|
|
75
|
+
The port preserves left-to-right accumulation order of the Rust source so that IEEE-754 results match for deterministic inputs. Results may differ from the Rust crate only when random initialization differs (see above).
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
GPL-3.0-or-later (same as upstream rust-kmedoids).
|
|
80
|
+
|
|
81
|
+
> This program is free software: you can redistribute it and/or modify
|
|
82
|
+
> it under the terms of the GNU General Public License as published by
|
|
83
|
+
> the Free Software Foundation, either version 3 of the License, or
|
|
84
|
+
> (at your option) any later version.
|
|
85
|
+
>
|
|
86
|
+
> This program is distributed in the hope that it will be useful,
|
|
87
|
+
> but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
88
|
+
> MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
89
|
+
> GNU General Public License for more details.
|
|
90
|
+
>
|
|
91
|
+
> You should have received a copy of the GNU General Public License
|
|
92
|
+
> along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
93
|
+
|
|
94
|
+
## References
|
|
95
|
+
|
|
96
|
+
> Erich Schubert and Lars Lenssen
|
|
97
|
+
> **Fast k-medoids Clustering in Rust and Python**
|
|
98
|
+
> Journal of Open Source Software 7(75), 4183
|
|
99
|
+
> <https://doi.org/10.21105/joss.04183>
|
|
100
|
+
|
|
101
|
+
> Erich Schubert, Peter J. Rousseeuw
|
|
102
|
+
> **Fast and Eager k-Medoids Clustering:
|
|
103
|
+
> O(k) Runtime Improvement of the PAM, CLARA, and CLARANS Algorithms**
|
|
104
|
+
> Information Systems (101), 2021, 101804
|
|
105
|
+
> <https://doi.org/10.1016/j.is.2021.101804>
|
|
106
|
+
|
|
107
|
+
> Erich Schubert, Peter J. Rousseeuw:
|
|
108
|
+
> **Faster k-Medoids Clustering: Improving the PAM, CLARA, and CLARANS Algorithms**
|
|
109
|
+
> In: 12th International Conference on Similarity Search and Applications (SISAP 2019), 171-187.
|
|
110
|
+
> <https://doi.org/10.1007/978-3-030-32047-8_16>
|
|
111
|
+
> Preprint: <https://arxiv.org/abs/1810.05691>
|
|
112
|
+
|
|
113
|
+
> Lars Lenssen, Erich Schubert:
|
|
114
|
+
> **Medoid silhouette clustering with automatic cluster number selection**
|
|
115
|
+
> Information Systems (120), 2024, 102290
|
|
116
|
+
> <https://doi.org/10.1016/j.is.2023.102290>
|
|
117
|
+
> Preprint: <https://arxiv.org/abs/2309.03751>
|
|
118
|
+
|
|
119
|
+
> Lars Lenssen, Erich Schubert:
|
|
120
|
+
> **Clustering by Direct Optimization of the Medoid Silhouette**
|
|
121
|
+
> In: 15th International Conference on Similarity Search and Applications (SISAP 2022)
|
|
122
|
+
> <https://doi.org/10.1007/978-3-031-17849-8_15>
|
|
123
|
+
|
|
124
|
+
If you use this code in scientific work, please cite the relevant papers above.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Alternating (k-means-style) k-medoids algorithm.
|
|
2
|
+
// Ported 1:1 from src/alternating.rs
|
|
3
|
+
|
|
4
|
+
import { arrayAdapter } from './arrayadapter.mjs';
|
|
5
|
+
import { USIZE_MAX, choose_medoid_within_partition } from './util.mjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Run the Alternating algorithm, a k-means-style alternate optimization.
|
|
9
|
+
*
|
|
10
|
+
* This is fairly fast (O(n²), like the FasterPAM method), but because the
|
|
11
|
+
* newly chosen medoid must cover the entire existing cluster, it tends to
|
|
12
|
+
* get stuck in worse local optima as the alternatives. Hence, it is not
|
|
13
|
+
* really recommended to use this algorithm (also known as "Alternate" in
|
|
14
|
+
* classic facility location literature, and re-invented by Park and Jun 2009).
|
|
15
|
+
*
|
|
16
|
+
* @param {*} mat - a pairwise distance matrix (2D array or adapter object)
|
|
17
|
+
* @param {number[]} med - the list of medoids (mutated in place)
|
|
18
|
+
* @param {number} maxiter - the maximum number of iterations allowed
|
|
19
|
+
* @returns {{ loss: number, assi: number[], nIter: number }}
|
|
20
|
+
*/
|
|
21
|
+
export function alternating(mat, med, maxiter) {
|
|
22
|
+
mat = arrayAdapter(mat);
|
|
23
|
+
const n = mat.len();
|
|
24
|
+
const assi = new Array(n).fill(USIZE_MAX);
|
|
25
|
+
let loss = assign_nearest(mat, med, assi);
|
|
26
|
+
let iter = 0;
|
|
27
|
+
while (iter < maxiter) {
|
|
28
|
+
iter += 1;
|
|
29
|
+
let changed = false;
|
|
30
|
+
for (let i = 0; i < med.length; i++) {
|
|
31
|
+
changed |= choose_medoid_within_partition(mat, assi, med, i)[0];
|
|
32
|
+
}
|
|
33
|
+
if (!changed) {
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
loss = assign_nearest(mat, med, assi);
|
|
37
|
+
}
|
|
38
|
+
return { loss, assi, nIter: iter };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Assign each point to the nearest medoid, return total loss.
|
|
43
|
+
* Mutates dataArr[i] = index into med of nearest medoid.
|
|
44
|
+
*
|
|
45
|
+
* @param {*} mat - already-wrapped matrix
|
|
46
|
+
* @param {number[]} med - medoid point indices
|
|
47
|
+
* @param {number[]} dataArr - assignment array, mutated in place
|
|
48
|
+
* @returns {number} total loss
|
|
49
|
+
*/
|
|
50
|
+
export function assign_nearest(mat, med, dataArr) {
|
|
51
|
+
const n = mat.len();
|
|
52
|
+
const k = med.length;
|
|
53
|
+
if (!mat.isSquare()) throw new Error('Dissimilarity matrix is not square');
|
|
54
|
+
if (n > 4294967295) throw new Error('N is too large');
|
|
55
|
+
if (!(k > 0 && k < 4294967295)) throw new Error('invalid N');
|
|
56
|
+
if (k > n) throw new Error('k must be at most N');
|
|
57
|
+
const firstcenter = med[0];
|
|
58
|
+
let acc = 0;
|
|
59
|
+
for (let i = 0; i < n; i++) {
|
|
60
|
+
let best = [0, mat.get(i, firstcenter)];
|
|
61
|
+
for (let m = 1; m < k; m++) {
|
|
62
|
+
const mm = med[m];
|
|
63
|
+
const dm = mat.get(i, mm);
|
|
64
|
+
if (dm < best[1] || i === mm) {
|
|
65
|
+
best = [m, dm];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
dataArr[i] = best[0];
|
|
69
|
+
acc += best[1];
|
|
70
|
+
}
|
|
71
|
+
return acc;
|
|
72
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Array adapters, ported from src/arrayadapter.rs
|
|
2
|
+
// All matrices expose: len() -> n, isSquare() -> bool, get(x, y) -> number
|
|
3
|
+
|
|
4
|
+
/** Wraps a dense 2D array (array of equal-length rows). */
|
|
5
|
+
export class DenseMatrix {
|
|
6
|
+
constructor(rows) { this.rows = rows; }
|
|
7
|
+
len() { return this.rows.length; }
|
|
8
|
+
isSquare() {
|
|
9
|
+
const n = this.rows.length;
|
|
10
|
+
for (let i = 0; i < n; i++) { if (this.rows[i].length !== n) return false; }
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
get(x, y) { return this.rows[x][y]; }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Lower triangular matrix in serial form (without diagonal). data length = n*(n-1)/2. */
|
|
17
|
+
export class LowerTriangle {
|
|
18
|
+
constructor(n, data) { this.n = n; this.data = data; }
|
|
19
|
+
len() { return this.n; }
|
|
20
|
+
isSquare() { return this.data.length === ((this.n * (this.n - 1)) / 2); }
|
|
21
|
+
get(x, y) {
|
|
22
|
+
// products x*(x-1) are always even, so /2 is an exact integer (valid up to 2^53);
|
|
23
|
+
// the original >>1 truncates to int32 and overflows the index for n >= ~46342
|
|
24
|
+
if (x < y) return this.data[((y * (y - 1)) / 2) + x];
|
|
25
|
+
if (x > y) return this.data[((x * (x - 1)) / 2) + y];
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Normalize user input into a matrix object. Idempotent. Accepts a 2D array, DenseMatrix, or LowerTriangle. */
|
|
31
|
+
export function arrayAdapter(input) {
|
|
32
|
+
if (input && typeof input.get === 'function' && typeof input.len === 'function') return input;
|
|
33
|
+
if (Array.isArray(input)) return new DenseMatrix(input);
|
|
34
|
+
throw new Error('Unsupported matrix input: provide a 2D array, a DenseMatrix, or a LowerTriangle');
|
|
35
|
+
}
|