1 | /* chinese-whisper version 0.3.9 */
|
2 | (function (global, factory) {
|
3 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('events')) :
|
4 | typeof define === 'function' && define.amd ? define(['exports', 'events'], factory) :
|
5 | (global = global || self, factory(global.window = {}, global.events));
|
6 | }(this, function (exports, events) { 'use strict';
|
7 |
|
8 | /**
|
9 | * This file was auto generated from scripts/generate-version.sh
|
10 | */
|
11 | const VERSION = '0.3.9';
|
12 |
|
13 | /**
|
14 | * Chinese Whispers Algorithm for Node.js
|
15 | *
|
16 | * https://github.com/huan/chinese-whispers
|
17 | * License: Apache-2.0
|
18 | * Author: Huan LI <zixia@zixia.net>
|
19 | *
|
20 | * Inspired by:
|
21 | * - http://blog.csdn.net/liyuan123zhouhui/article/details/70312716
|
22 | * - http://alexloveless.co.uk/data/chinese-whispers-graph-clustering-in-python/
|
23 | * - https://github.com/uhh-lt/chinese-whispers
|
24 | * - https://github.com/anvaka/ngraph.cw
|
25 | *
|
26 | */
|
27 | const jsnx = require('jsnetworkx');
|
28 | const { knuthShuffle } = require('knuth-shuffle');
|
29 | class ChineseWhispers extends events.EventEmitter {
|
30 | constructor(options) {
|
31 | super();
|
32 | this.weightFunc = options.weightFunc;
|
33 | this.epochs = options.epochs || 15;
|
34 | this.threshold = options.threshold;
|
35 | this.on('change', () => this.changeCounter++);
|
36 | }
|
37 | on(event, listener) {
|
38 | super.on(event, listener);
|
39 | return this;
|
40 | }
|
41 | emit(event, ...args) {
|
42 | return super.emit(event, ...args);
|
43 | }
|
44 | cluster(dataList) {
|
45 | // this.dataList = dataList
|
46 | this.graph = this.buildNetwork(dataList, this.weightFunc, this.threshold);
|
47 | // initial epoch
|
48 | this.emit('epoch', this.graph, -1);
|
49 | // run Chinese Whispers
|
50 | // I default to 10 iterations. This number is usually low.
|
51 | // After a certain number (individual to the data set) no further clustering occurs
|
52 | let epochs = this.epochs;
|
53 | while (epochs--) {
|
54 | this.changeCounter = 0;
|
55 | this.iterate();
|
56 | this.emit('epoch', this.graph, this.changeCounter);
|
57 | }
|
58 | const clusterList = this.buildClusterList(this.graph);
|
59 | return clusterList;
|
60 | }
|
61 | iterate() {
|
62 | const nodeList = this.graph.nodes();
|
63 | // I randomize the nodes to give me an arbitrary start point
|
64 | knuthShuffle(nodeList); // orignal array modified
|
65 | for (const node of nodeList) {
|
66 | this.relabelNode(node);
|
67 | }
|
68 | }
|
69 | relabelNode(node) {
|
70 | const newLabel = this.recalcLabel(node);
|
71 | const nodeAttr = this.graph.node.get(node);
|
72 | if (nodeAttr.label !== newLabel) {
|
73 | // set the label of target node to the winning local label
|
74 | this.emit('change', node, nodeAttr.label, newLabel);
|
75 | nodeAttr.label = newLabel;
|
76 | // console.log('set node ' + this.data[node] + ' label to ' + this.data[parseInt(newLabel)])
|
77 | }
|
78 | }
|
79 | recalcLabel(node) {
|
80 | const nxNode = this.graph.get(node);
|
81 | const neighborList = this.graph.neighbors(node);
|
82 | if (neighborList.length === 0) {
|
83 | const attr = this.graph.node.get(node);
|
84 | return attr.label; // return self label if no neighbors
|
85 | }
|
86 | // console.log('neighbors of node ' + this.data[node]
|
87 | // + ' is: ', neighborList.map((i: number) => this.data[i]))
|
88 | const labelWeightMap = {};
|
89 | // do an inventory of the given nodes neighbours and edge weights
|
90 | for (const neighbor of neighborList) {
|
91 | const neighborAttr = this.graph.node.get(neighbor);
|
92 | const label = neighborAttr.label;
|
93 | if (!(label in labelWeightMap)) {
|
94 | labelWeightMap[label] = 0;
|
95 | }
|
96 | const edgeAttr = nxNode.get(neighbor);
|
97 | const weight = edgeAttr.weight;
|
98 | // console.log('### ', weight, ' ###')
|
99 | labelWeightMap[label] += weight;
|
100 | }
|
101 | // find the label with the highest edge weight sum
|
102 | let max = 0;
|
103 | // In javascript the keys of object can only be strings
|
104 | // - https://stackoverflow.com/a/41870625/1123955
|
105 | let maxLabel = '-1';
|
106 | // for (const label in labelWeightMap) {
|
107 | // console.log('labelWeight node: ' + this.data[node] + ' - '
|
108 | // + this.data[parseInt(label)] + ' is: ', labelWeightMap[label])
|
109 | // }
|
110 | for (const label in labelWeightMap) {
|
111 | if (labelWeightMap[label] > max) {
|
112 | max = labelWeightMap[label];
|
113 | maxLabel = label;
|
114 | }
|
115 | }
|
116 | return maxLabel;
|
117 | }
|
118 | buildClusterList(G) {
|
119 | const clusterList = [];
|
120 | const labelIndexMap = {};
|
121 | let index = 0;
|
122 | for (const node of G.nodes()) {
|
123 | const label = G.node.get(node).label;
|
124 | if (!(label in labelIndexMap)) {
|
125 | labelIndexMap[label] = index++;
|
126 | }
|
127 | const labelIndex = labelIndexMap[label];
|
128 | if (!(labelIndex in clusterList)) {
|
129 | clusterList[labelIndex] = [];
|
130 | }
|
131 | clusterList[labelIndex].push(node);
|
132 | }
|
133 | return clusterList;
|
134 | }
|
135 | buildNetwork(dataList, weightFunc, threshold) {
|
136 | const nodeList = Object.keys(dataList).map(k => parseInt(k, 10)); // [0, 1, 2, ..., data.length - 1]
|
137 | // const nodeList: CWNode[] = [...data.keys()] // [0, 1, 2, ..., data.length - 1]
|
138 | const edgeList = [];
|
139 | for (let i = 0; i < dataList.length; i++) {
|
140 | for (let j = i + 1; j < dataList.length; j++) {
|
141 | const weight = weightFunc(dataList[i], dataList[j]);
|
142 | if (threshold && threshold > weight) {
|
143 | // console.log('threshold: ', threshold, ' weight: ', weight)
|
144 | // skip this edge because it's weight is below threshold
|
145 | this.emit('edge');
|
146 | }
|
147 | else {
|
148 | const edge = [i, j, { weight }];
|
149 | edgeList.push(edge);
|
150 | this.emit('edge', edge);
|
151 | }
|
152 | }
|
153 | }
|
154 | // initialize the graph
|
155 | const G = new jsnx.Graph();
|
156 | // Add nodes
|
157 | G.addNodesFrom(nodeList);
|
158 | // CW needs an arbitrary, unique label for each node before initialisation
|
159 | // Here I use the ID of the node since I know it's unique
|
160 | // You could use a random number or a counter or anything really
|
161 | for (const n of G.nodes()) {
|
162 | const nodeAttr = G.node.get(n);
|
163 | nodeAttr.label = n;
|
164 | }
|
165 | // add edges
|
166 | G.addEdgesFrom(edgeList);
|
167 | // console.log(edgeList)
|
168 | return G;
|
169 | }
|
170 | }
|
171 |
|
172 | exports.ChineseWhispers = ChineseWhispers;
|
173 | exports.VERSION = VERSION;
|
174 | exports.default = ChineseWhispers;
|
175 |
|
176 | Object.defineProperty(exports, '__esModule', { value: true });
|
177 |
|
178 | }));
|
179 | /* https://git.io/zixia/ */
|
180 | //# sourceMappingURL=chinese-whispers.es6.umd.js.map
|