Skip to content

Commit a57f8e9

Browse files
committed
k-means clustering of armies
show games with more than a certain # of a certain unit at a certain time filter by whether the match is from a frequent uploader
1 parent 2403b2a commit a57f8e9

File tree

11 files changed

+560
-38
lines changed

11 files changed

+560
-38
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,5 @@ node_modules
4545
# Ignore temp data used for development
4646
public/ents*
4747
public/matches*
48+
public/minutes*
49+
public/frequent*

Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,5 @@ gem 'curb'
142142
#gem 'aws-s3'
143143

144144
gem 'activemerchant'
145+
146+
gem 'nokogiri', '1.6.3.1'

Gemfile.lock

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ GEM
160160
mime-types (~> 1.16)
161161
treetop (~> 1.4.8)
162162
mime-types (1.25.1)
163-
mini_portile (0.5.2)
163+
mini_portile (0.6.0)
164164
money (5.1.1)
165165
i18n (~> 0.6.0)
166166
mono_logger (1.1.0)
@@ -170,8 +170,8 @@ GEM
170170
net-http-spy (0.2.1)
171171
netrc (0.7.7)
172172
newrelic_rpm (3.9.1.236)
173-
nokogiri (1.6.1)
174-
mini_portile (~> 0.5.0)
173+
nokogiri (1.6.3.1)
174+
mini_portile (= 0.6.0)
175175
orm_adapter (0.4.0)
176176
paperclip (4.1.1)
177177
activemodel (>= 3.0.0)
@@ -309,6 +309,7 @@ DEPENDENCIES
309309
mysql2
310310
net-http-spy
311311
newrelic_rpm
312+
nokogiri (= 1.6.3.1)
312313
paperclip
313314
paperclip-aws
314315
rails (= 3.2.19)

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ On Mac OSX, you can use homebrew as package manager: http://mxcl.github.com/home
2929
### Basic installation and updating
3030

3131
* Run Bundler (`bundle`)
32+
** On Mac OSX, you may run into hassle with nokogiri: http://stackoverflow.com/a/21616065
3233
* Copy and adjust database configuration (`cp config/database.yml.example config/database.yml`)
3334
* Copy secrets configuration (`cp config/secrets.yml.example config/secrets.yml`)
3435
* Copy s3 configuration (`cp config/s3.yml.example config/s3.yml`)

app/assets/javascripts/angular/controllers/scout.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,74 @@ gg.controller('ScoutController', ['$scope', '$element', '$urlFilter',
1515
console.log('refresh!', params);
1616
}
1717

18+
NUM_CLUSTERS = 10;
19+
20+
$scope.compute_groups = function() {
21+
var start1 = Date.now();
22+
var start2 = Date.now();
23+
var all_gamerecords = dateDim.top(Infinity);
24+
var armies = _.map(all_gamerecords, function(gr) {
25+
return gr.player.army_vector(8);
26+
});
27+
var start3 = Date.now();
28+
kmeans_output = clusterfck.kmeans(armies, NUM_CLUSTERS);
29+
var end = Date.now();
30+
var total_time = end - start1;
31+
console.log("kmeans took " + (total_time / 1000) + " seconds");
32+
33+
var centroids = kmeans_output[0];
34+
var clusters = kmeans_output[1];
35+
var assignment = kmeans_output[2];
36+
37+
// make groups an array of empty arrays, one for each cluster
38+
var cluster_gamerecords = _.map(clusters, function(){return []});
39+
40+
// put the gamerecords for each cluster into its respective array
41+
_.each(assignment, function(gr_assign, index) {
42+
cluster_gamerecords[gr_assign].push(all_gamerecords[index]);
43+
});
44+
45+
// then compute $scope.groups as aggregate functions of arrays of gamerecords
46+
var groupstats = _.map(clusters, function(cluster, cluster_index) {
47+
var groupstat = { count: cluster_gamerecords[cluster_index].length,
48+
gamerecords: cluster_gamerecords[cluster_index]
49+
};
50+
var armySum = _.map(_.range(0,19), function() { return 0.0 });
51+
var numWins = 0;
52+
_.each(cluster_gamerecords[cluster_index], function(gamerecord) {
53+
_.each(_.range(0,19), function(army_index) {
54+
armySum[army_index] += gamerecord.player.stat(8, 'u' + army_index);
55+
});
56+
if (gamerecord.player.win == 'true') {
57+
numWins++;
58+
}
59+
});
60+
armySum = _.map(armySum, function(unit_count) {
61+
var average_count = unit_count / groupstat.count;
62+
if (average_count < 0.1) {
63+
return '';
64+
}
65+
if (average_count >= 10.0) {
66+
return Math.round(average_count);
67+
}
68+
return Math.floor(average_count * 10.0) / 10.0;
69+
70+
});
71+
groupstat.unit_counts = armySum;
72+
groupstat.winPct = Math.floor(100.0 * numWins / groupstat.count);
73+
groupstat.click = function() {
74+
// console.log("groupstat clicked!", this);
75+
$scope.groupsub = this.gamerecords;
76+
};
77+
78+
return groupstat;
79+
});
80+
81+
$scope.groupstats = _.sortBy(groupstats, function(groupstat) {
82+
return -1 * groupstat.winPct;
83+
});
84+
}
85+
1886
$scope.$watch('race + vs_race', function(v) {
1987
$scope.filter.params.race = $scope.race;
2088
$scope.filter.params.vs_race = $scope.vs_race;
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
clusterfck = {}
2+
3+
clusterfck.distances = {
4+
euclidean: function(v1, v2) {
5+
var total = 0;
6+
for (var i = 0; i < v1.length; i++) {
7+
total += Math.pow(v2[i] - v1[i], 2);
8+
}
9+
return Math.sqrt(total);
10+
},
11+
manhattan: function(v1, v2) {
12+
var total = 0;
13+
for (var i = 0; i < v1.length ; i++) {
14+
total += Math.abs(v2[i] - v1[i]);
15+
}
16+
return total;
17+
},
18+
max: function(v1, v2) {
19+
var max = 0;
20+
for (var i = 0; i < v1.length; i++) {
21+
max = Math.max(max , Math.abs(v2[i] - v1[i]));
22+
}
23+
return max;
24+
}
25+
};
26+
27+
function randomCentroids(points, k) {
28+
var centroids = points.slice(0); // copy
29+
centroids.sort(function() {
30+
return (Math.round(Math.random()) - 0.5);
31+
});
32+
return centroids.slice(0, k);
33+
}
34+
35+
function closestCentroid(point, centroids, distance) {
36+
var min = Infinity,
37+
index = 0;
38+
for (var i = 0; i < centroids.length; i++) {
39+
var dist = distance(point, centroids[i]);
40+
if (dist < min) {
41+
min = dist;
42+
index = i;
43+
}
44+
}
45+
return index;
46+
}
47+
48+
function kmeans(points, k, distance, snapshotPeriod, snapshotCb) {
49+
k = k || Math.max(2, Math.ceil(Math.sqrt(points.length / 2)));
50+
51+
distance = distance || "euclidean";
52+
if (typeof distance == "string") {
53+
distance = clusterfck.distances[distance];
54+
}
55+
56+
var centroids = randomCentroids(points, k);
57+
var assignment = new Array(points.length);
58+
var clusters = new Array(k);
59+
60+
var iterations = 0;
61+
var movement = true;
62+
while (movement) {
63+
// update point-to-centroid assignments
64+
for (var i = 0; i < points.length; i++) {
65+
assignment[i] = closestCentroid(points[i], centroids, distance);
66+
}
67+
68+
// update location of each centroid
69+
movement = false;
70+
for (var j = 0; j < k; j++) {
71+
var assigned = [];
72+
for (var i = 0; i < assignment.length; i++) {
73+
if (assignment[i] == j) {
74+
assigned.push(points[i]);
75+
}
76+
}
77+
78+
if (!assigned.length) {
79+
continue;
80+
}
81+
var centroid = centroids[j];
82+
var newCentroid = new Array(centroid.length);
83+
84+
for (var g = 0; g < centroid.length; g++) {
85+
var sum = 0;
86+
for (var i = 0; i < assigned.length; i++) {
87+
sum += assigned[i][g];
88+
}
89+
newCentroid[g] = sum / assigned.length;
90+
91+
if (newCentroid[g] != centroid[g]) {
92+
movement = true;
93+
}
94+
}
95+
centroids[j] = newCentroid;
96+
clusters[j] = assigned;
97+
}
98+
99+
if (snapshotCb && (iterations++ % snapshotPeriod == 0)) {
100+
snapshotCb(clusters);
101+
}
102+
}
103+
return [centroids, clusters, assignment];
104+
}
105+
106+
clusterfck.kmeans = kmeans;

app/assets/javascripts/gg.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
//= require d3.js
7474
//= require crossfilter.js
7575
//= require scout.js
76+
//= require ./clusterfck/kmeans.js
7677

7778
function dontDoThisEither() {
7879
// Proxino.log("log this");

0 commit comments

Comments
 (0)