UNPKG

7.57 kBJavaScriptView Raw
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