package org.activityinfo.server.report.generator.map.cluster.genetic; /* * #%L * ActivityInfo Server * %% * Copyright (C) 2009 - 2013 UNICEF * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program. If not, see * <http://www.gnu.org/licenses/gpl-3.0.html>. * #L% */ import org.activityinfo.legacy.shared.reports.content.Point; import org.activityinfo.server.report.generator.map.cluster.Cluster; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * Finds a given number of cluster centers using the KMeans algorithm. * * @author Alex Bertram */ public final class KMeans { private KMeans() { } public static List<Cluster> cluster(List<MarkerGraph.Node> nodes, int numClusters) { List<Cluster> clusters = new ArrayList<Cluster>(numClusters); // sanity check if (numClusters > nodes.size() || nodes.size() == 0) { throw new IllegalArgumentException(); } // randomize Collections.shuffle(nodes); // choose random centers Point[] centers = new Point[numClusters]; for (int i = 0; i != numClusters; ++i) { centers[i] = nodes.get(i).getPoint(); } // assign initial cluster membership int[] membership = new int[nodes.size()]; assignClosest(nodes, centers, membership); // execute k-means algorithm until we achieve convergence boolean changed; do { computeCenters(nodes, membership, centers); changed = assignClosest(nodes, centers, membership); } while (changed); // create clusters for (int i = 0; i != numClusters; ++i) { clusters.add(new Cluster(centers[i])); } for (int j = 0; j != nodes.size(); ++j) { (clusters.get(membership[j])).addNode(nodes.get(j)); } return clusters; } /** * Computes the centers of the assigned clusters * * @param nodes The list of nodes * @param membership An array containing the cluster membership for each node * @param centers Array of center points to update */ private static void computeCenters(List<MarkerGraph.Node> nodes, int[] membership, Point[] centers) { int[] sumX = new int[centers.length]; int[] sumY = new int[centers.length]; int[] counts = new int[centers.length]; for (int i = 0; i != nodes.size(); ++i) { Point point = nodes.get(i).getPoint(); sumX[membership[i]] += point.getX(); sumY[membership[i]] += point.getY(); counts[membership[i]]++; } for (int i = 0; i != centers.length; ++i) { if (counts[i] > 0) { centers[i] = new Point(sumX[i] / counts[i], sumY[i] / counts[i]); } } } /** * Updates the membership array by assigning each node to its closest * cluster * * @param nodes * @param centers * @param membership * @return True if cluster membership has changed */ private static boolean assignClosest(List<MarkerGraph.Node> nodes, Point[] centers, int[] membership) { boolean changed = false; for (int i = 0; i != nodes.size(); ++i) { // for this node, find the closest // cluster center double minDist = Double.MAX_VALUE; int closest = 0; for (int j = 0; j != centers.length; ++j) { double dist = nodes.get(i).getPoint().distance(centers[j]); if (dist < minDist) { minDist = dist; closest = j; } } // update membership if necessary if (membership[i] != closest) { membership[i] = closest; changed = true; } } return changed; } }