UNPKG

als-statistics

Version:

A powerful and lightweight JavaScript library for descriptive statistics, regression, clustering, outlier detection, and noise analysis using a flexible table/column architecture.

215 lines (190 loc) 7.35 kB
class Hdbscan { constructor(table, minClusterSize = 2) { if (table.columnsN < 2) throw new Error('2 or more columns required'); this.table = table; this.minClusterSize = minClusterSize; this.columnsArray = Object.entries(table.columns); this.n = this.columnsArray.length; this.labels = new Array(this.n).fill(-1); this.clusters = []; this.distances = null; this.mreachDistances = null; this.mst = []; this.hierarchy = []; this.computeDistances(); this.computeMutualReachability(); this.buildMST(); this.buildHierarchy(); this.extractClusters(); this.buildClusters(); } computeDistances() { this.distances = new Array(this.n).fill(null).map(() => new Array(this.n).fill(null)); for (let i = 0; i < this.n; i++) { for (let j = i + 1; j < this.n; j++) { const { correlationSample } = this.table.compare(this.columnsArray[i][0], this.columnsArray[j][0]); const dist = isNaN(correlationSample) ? 1 : 1 - correlationSample; this.distances[i][j] = dist; this.distances[j][i] = dist; } this.distances[i][i] = 0; } } computeMutualReachability() { this.mreachDistances = new Array(this.n).fill(null).map(() => new Array(this.n).fill(null)); const coreDistances = new Array(this.n); for (let i = 0; i < this.n; i++) { const distances = this.distances[i].slice().sort((a, b) => a - b); coreDistances[i] = distances[Math.min(this.minClusterSize - 1, this.n - 1)]; } for (let i = 0; i < this.n; i++) { for (let j = i; j < this.n; j++) { const mreach = Math.max(coreDistances[i], coreDistances[j], this.distances[i][j]); this.mreachDistances[i][j] = mreach; this.mreachDistances[j][i] = mreach; } } } buildMST() { const visited = new Array(this.n).fill(false); const key = new Array(this.n).fill(Infinity); const parent = new Array(this.n).fill(-1); key[0] = 0; for (let count = 0; count < this.n - 1; count++) { let minKey = Infinity; let u = -1; for (let v = 0; v < this.n; v++) { if (!visited[v] && key[v] < minKey) { minKey = key[v]; u = v; } } visited[u] = true; if (parent[u] !== -1) this.mst.push([parent[u], u, this.mreachDistances[parent[u]][u]]); for (let v = 0; v < this.n; v++) { if (!visited[v] && this.mreachDistances[u][v] < key[v]) { key[v] = this.mreachDistances[u][v]; parent[v] = u; } } } this.mst.sort((a, b) => a[2] - b[2]); } buildHierarchy() { const uf = new UnionFind(this.n); const clusterSizes = new Array(this.n).fill(1); let clusterId = 0; // Начинаем с 0 для уникальных кластеров this.hierarchy = []; for (let i = 0; i < this.n; i++) { this.hierarchy.push({ clusterId: i, lambdaBirth: 0, lambdaDeath: Infinity, points: [i], size: 1 }); } for (const [p, q, weight] of this.mst) { const lambda = 1 / weight; const cp = uf.find(p); const cq = uf.find(q); if (cp !== cq) { const sizeP = clusterSizes[cp]; const sizeQ = clusterSizes[cq]; uf.union(cp, cq); const newRoot = uf.find(cp); clusterSizes[newRoot] = sizeP + sizeQ; // Обновляем lambdaDeath для старых кластеров this.hierarchy[cp].lambdaDeath = lambda; this.hierarchy[cq].lambdaDeath = lambda; // Добавляем новый кластер this.hierarchy.push({ clusterId: clusterId + this.n, lambdaBirth: lambda, lambdaDeath: Infinity, points: this.hierarchy[cp].points.concat(this.hierarchy[cq].points), size: sizeP + sizeQ }); clusterId++; } } } extractClusters() { // Вычисляем стабильность каждого кластера const stability = new Map(); this.hierarchy.forEach(cluster => { if (cluster.size >= this.minClusterSize) { const stabilityValue = cluster.size * (cluster.lambdaDeath - cluster.lambdaBirth); stability.set(cluster.clusterId, stabilityValue); } }); // Извлекаем кластеры, сравнивая стабильность const activeClusters = new Map(); this.hierarchy.forEach(cluster => { if (cluster.size < this.minClusterSize) return; let isStable = true; const childrenStability = cluster.points.reduce((sum, p) => { const child = this.hierarchy.find(c => c.clusterId === p); return sum + (stability.get(child.clusterId) || 0); }, 0); if (childrenStability > stability.get(cluster.clusterId)) { isStable = false; } if (isStable) { activeClusters.set(cluster.clusterId, cluster.points); } }); // Присваиваем метки точкам let clusterId = 1; const assigned = new Set(); activeClusters.forEach((points, id) => { if (points.length >= this.minClusterSize) { points.forEach(p => { if (!assigned.has(p)) { this.labels[p] = clusterId; assigned.add(p); } }); clusterId++; } }); // Отладка console.log('Points:', this.n); console.log('Labels:', this.labels); console.log('Clusters found:', new Set(this.labels.filter(l => l !== -1)).size); } buildClusters() { const maxLabel = Math.max(...this.labels); if (maxLabel < 1) { console.error('No clusters found'); return; } for (let id = 1; id <= maxLabel; id++) { const clusterTable = new this.table.Table(); this.columnsArray.forEach(([name, column], idx) => { if (this.labels[idx] === id) { clusterTable.addColumn(name, column.clone(true)); } }); if (clusterTable.columnsN > 0) this.clusters.push(clusterTable); } } } class UnionFind { constructor(n) { this.parent = new Array(n).fill(null).map((_, i) => i); this.rank = new Array(n).fill(0); } find(x) { if (this.parent[x] !== x) this.parent[x] = this.find(this.parent[x]); return this.parent[x]; } union(x, y) { const px = this.find(x); const py = this.find(y); if (px === py) return; if (this.rank[px] < this.rank[py]) { this.parent[px] = py; } else if (this.rank[px] > this.rank[py]) { this.parent[py] = px; } else { this.parent[py] = px; this.rank[px]++; } } } module.exports = Hdbscan;