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
JavaScript
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;