地图聚类算法

时间:2009-09-16 16:58:45

标签: php performance algorithm google-maps cluster-analysis

我目前的代码很快,但我需要更快,所以我们可以容纳更多的标记。有什么建议吗?

注意:

  • 当SQL语句按标记名称排序时,代码运行得最快 - 它本身在聚类标记方面做了非常局部的工作(同一位置的标记名称通常但不总是相似)。
  • 我无法对标记进行预聚类,因为它们可以动态搜索和过滤。
  • 我尝试过基于网格的聚类 - 但结果往往不是很好。
  • 我知道墨卡托投影上的星团略微倾斜。
  • 我对商业集群服务不感兴趣。

代码:

$singleMarkers = array();
$clusterMarkers = array();

while (count($markers)) {
    $marker  = array_pop($markers);
    $cluster = array();

    // Compare marker against all remaining markers.
    foreach ($markers as $key => $compareMarker) {
        // This function returns the distance between two markers, at a defined zoom level.
        $pixels = pixelDistance($marker['lat'], $marker['lng'], $compareMarker['lat'], $compareMarker['lng'], $zoomLevel);
        // If two markers are closer than defined distance, remove compareMarker from array and add to cluster.
        if ($pixels < $distance) {
            unset($markers[$key]);
            $cluster[] = $compareMarker;
        }
    }

    // If a marker was added to cluster, also add the marker we were comparing to.
    if (count($cluster) > 0) {
        $cluster[] = $marker;
        $clusterMarkers[] = $cluster;
    } else {
        $singleMarkers[] = $marker;
    }
}

function pixelDistance($lat1, $lon1, $lat2, $lon2, $zoom) {
    $x1 = $lon1*10000000; //This is what I did to compensate for using lat/lon values instead of pixels.
    $y1 = $lat1*10000000;
    $x2 = $lon2*10000000;
    $y2 = $lat2*10000000;

    return sqrt(pow(($x1-$x2),2) + pow(($y1-$y2),2)) >> (21 - $zoom); //21 is the max zoom level
}

更新

这是当前的代码:

$singleMarkers = array();
$clusterMarkers = array();

// Minimum distance between markers to be included in a cluster, at diff. zoom levels
$DISTANCE = (10000000 >> $ZOOM) / 100000;

// Loop until all markers have been compared.
while (count($markers)) {
    $marker  = array_pop($markers);
    $cluster = array();

    // Compare against all markers which are left.
    foreach ($markers as $key => $target) {
        $pixels = abs($marker['lat']-$target['lat']) + abs($marker['lng']-$target['lng']);

        // If the two markers are closer than given distance remove target marker from array and add it to cluster.
        if ($pixels < $DISTANCE) {
            unset($markers[$key]);
            $cluster[] = $target;
        }
    }

    // If a marker has been added to cluster, add also the one we were comparing to.
    if (count($cluster) > 0) {
        $cluster[] = $marker;
        $clusterMarkers[] = $cluster;
    } else {
        $singleMarkers[] = $marker;
    }
}

8 个答案:

答案 0 :(得分:7)

你真的需要计算Euclidean distance吗?如果您只是比较相对距离的大小,您可以使用Manhattan distance,这可以为您节省两次pow()和一次sqrt()的调用:

function pixelDistance($lat1, $lon1, $lat2, $lon2, $zoom) {
    $x1 = $lon1*10000000; //This is what I did to compensate for using lat/lon values instead of pixels.
    $y1 = $lat1*10000000;
    $x2 = $lon2*10000000;
    $y2 = $lat2*10000000;

    return ($x1-$x2) + ($y1-$y2) >> (21 - $zoom);
}

不确定你的计算是否需要>> (21 - $zoom)位,所以我把它留在了。但除非你真的需要在其他地方使用计算的距离值,否则你可能只需要使用纬度/经度就可以了直接(不需要乘以任何东西)并取曼哈顿距离,假设您预先计算$distance以适应该度量,这将在计算方面比强迫所有距离适应更便宜$distance的单位和幅度。

编辑:当我去年研究这个问题时,我在维基百科上找到了一些有用的东西 - 是的,它可以发生; - )

我还强烈推荐 Programming Collective Intelligence: Building Smart Web 2.0 Applications 这本书,它非常深入地进行聚类,不仅适用于地理数据,也适用于其他数据分析领域。

答案 1 :(得分:4)

扩展约翰所说的,我认为你应该尝试内联这个功能。 PHP中的函数调用非常慢,所以你应该从中得到一个不错的加速。

答案 2 :(得分:2)

所以这就是我所做的 - 我在标记(点)表中添加了两个额外的列,使用以下函数为经纬度转换了纬度和经度值:

public static $offset = 268435456;
public static $radius = 85445659.44705395; /* $offset / pi(); */

function LonToX($lon)
{
    return round(self::$offset + self::$radius * $lon * pi() / 180);        
}

function LatToY($lat)
{
    return round(self::$offset - self::$radius * log((1 + sin($lat * pi() / 180)) / (1 - sin($lat * pi() / 180))) / 2);
}

通过这种方式,我可以准确地放置群集。我还在努力弄清楚如何避免每次都使用array_pop和循环。到目前为止,使用低于1000的标记非常有效。我稍后会发布+ 5K和+ 10K标记的结果。

避免使用pixelDistance功能并使其内联可将性能提高近一半!

答案 3 :(得分:1)

似乎加速pixelDistance()函数可能是您解决方案的一部分,因为它在循环内运行。这是一个首先看的好地方,但你没有包含那些代码,所以我不能确定。

答案 4 :(得分:1)

我可以在这里看到两个可能的改进:

  • 您可以循环浏览$ markers 使用for循环而不是弹出 他们离阵?数组弹出是完全不需要的 - 如果您同时向它们添加和删除项目(您不是;您只是处理它们然后扔掉它们),您应该只使用数组作为队列

  • 你应该尝试计算 数组的count() 开始,然后手动增加 或减少$ count变量。 重新计算数组的大小 每个循环都是浪费。

答案 5 :(得分:1)

以下是您可以实施的一些想法,以防性能出现问题:

  • 降低维度 数据:你有2d长/纬数据, 也许你可以尝试投射它 用类似的东西降到1D Multidimensional Scaling (MDS)哪个 试图减少数量 尺寸同时保留 原始空间中的距离,因此距离函数只需要处理一个特征而不是两个特征。另一种方法是使用Principal component analysis (PCA)
  • 更快的搜索:计算的步骤 到每个标记的距离可以是 使用KD-trees进行了改进。

这两种技术都应用于离线设置,因此通常会计算一次,然后多次使用..

答案 6 :(得分:1)

一个简单的优化就是利用sqrt(x)&lt;如果x <&lt; sqf(y),则为真。 y,所以你可以省略pixelDistance中的sqrt并计算循环外的$ distance平方。您还可以在循环外计算21 - $ zoomLevel,如果您要比较平方值,则必须将它乘以2。另一个小优化是在缩放10000000之前通过执行$ x1- $ x2来节省2倍。而对于更多一点,将delta存储在var中并将其自身相乘可能比pow函数更快。而对于更多内容,您可以内联像素距离函数。这种优化只会产生一个恒定的因子加速。

对于更大的加速,您需要某种加速度数据结构。一个简单的方法是将标记分成距离大小的正方形。然后你可以在垃圾箱上方查找标记,只在同一个垃圾箱中聚类,另外选择3个其他垃圾箱,这取决于标记落在垃圾箱中心的哪一侧。这将导致线性时间聚类,这将超过对更大结果集的二次算法的任何优化。

答案 7 :(得分:1)

如果可以,请在初始搜索时按经度对标记进行排序;然后,一旦标记超过排序列表中下一个标记的标记宽度,您肯定知道其余标记不会重叠,因此您可以打破foreach循环并节省大量处理时间。我已经在我自己的网站上实现了它,它的工作效率很高。

我在Python here中有一些源代码。