地图聚类算法

Posted

技术标签:

【中文标题】地图聚类算法【英文标题】:Map Clustering Algorithm 【发布时间】:2010-11-28 21:15:40 【问题描述】:

我当前的代码非常快,但我需要让它更快,以便我们可以容纳更多的标记。有什么建议吗?

注意事项:

当 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;
    

【问题讨论】:

【参考方案1】:

你真的需要计算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);

不确定您是否需要 &gt;&gt; (21 - $zoom) 位进行计算,所以我将其保留。但除非您确实需要在其他地方使用计算出的距离值,否则您可能直接使用纬度/经度就可以逃脱(无需乘以任何东西)并取曼哈顿距离,假设您预先计算 $distance 以适应该度量,这在计算方面比强制所有距离适应单位和幅度要便宜得多$distance.

编辑:当我去年研究这个问题时,我在 Wikipedia 上发现了一些有用的东西 - 是的,它可能会发生 ;-)

Cluster analysis Hierarchical clustering

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

【讨论】:

这很好用。 >> (21 - $zoom) 是我将缩放级别应用于方程式的方式 - 但我认为在算法中摆脱它并根据缩放计算合格的 $distance 会更快 - 所以我只有做一次。谢谢! 另外,方程确实需要使用绝对值:$pixels = abs($x1-$x2) + abs($y1-$y2);【参考方案2】:

扩展 John 所说的内容,我认为您应该尝试内联该函数。 php 中的函数调用非常慢,因此您应该从中获得不错的加速。

【讨论】:

太棒了!这实际上相当重要。【参考方案3】:

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

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

这两种技术都是在离线设置中应用的,因此它们通常只计算一次,然后多次使用..

【讨论】:

【参考方案4】:

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

【讨论】:

好点 - 我添加了这个功能。在 pixelDistance() 中,我之前将 lat/lon 值转换为像素值 - 但我发现如果我只是使用直接向上的 lat/lon 值进行聚类,我可以加快速度。即使它稍微倾斜了集群。【参考方案5】:

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

你能不能循环遍历 $markers 使用 for 循环而不是弹出 他们离开阵列?数组弹出是完全不需要的——如果你同时向它们添加和删除项目,你真的应该只使用数组作为队列(你不是;你只是在处理它们然后把它们扔掉)

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

【讨论】:

每次循环重新计算数组的大小确实很浪费,但我不知道如何摆脱它,以当前的结构。在给定的循环中,数组可能会发生显着变化——当我针对每个剩余的标记检查弹出的标记时,它们中的任何一个都可以添加到一个簇中,从而从数组中删除。在高缩放级别下,阵列中的所有或大部分标记将被移除并放置到一个集群中,没有留下任何标记可供检查。这显着减少了完成所需的循环次数。我希望这是有道理的。 :)【参考方案6】:

一个简单的优化是利用 sqrt(x)

要获得更大的加速,您需要某种加速数据结构。一个简单的方法是将标记分成距离大小的正方形。然后,您可以遍历这些垃圾箱,仅在同一个垃圾箱中寻找要聚类的标记,并根据标记落在垃圾箱中心的哪一侧选择其他 3 个。这将导致线性时间聚类,这将击败对更大结果集的二次算法的任何优化。

【讨论】:

推迟 10000000 比例是个好主意,我不确定你计算循环外距离的意思。我需要检查每个标记之间的距离 - 循环就是发生这种情况的地方。此外,虽然 bin 的想法肯定会提高速度,但我不愿意这样做,因为它会显着降低集群的质量。 垃圾箱将产生与当前算法完全相同的结果。分箱将避免必须计算可能不够近的标记的距离。 好的,我想我明白你现在在说什么了——即使两个接近的标记被一个 bin 边界分开,也没关系,因为我可以检查相邻的 bin。我会看看我是否能弄清楚如何实现这一点。 我无法找到一种比我更快的方法来实现它。可能有办法做到这一点...... :( 给你:gist.github.com/204365 我添加了一些 cmets 以便于理解。【参考方案7】:

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

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 函数并将其内联提高了近一半的性能!

【讨论】:

实际上 - 我无法将 cmets 添加到原始问题或任何其他答案中,除非答案是我的……很奇怪。无论如何,看起来您正在运行 Tuupola 代码的修改版本 (github.com/tuupola/php_google_maps)。我已经描述了你到目前为止所拥有的东西,当你有数千个点时,它绝对没有效率。我一直在为各种解决方案苦苦挣扎,但这一切仍然指向预聚类——如果想要动态生成数据,这是一个问题。 我实际上并没有将 lat 和 lon 值转换为 x 和 y 值 - 我只是将它们按原样聚类。这意味着当星团远离赤道时,它们会变得更加偏斜,但这几乎不会引起注意。我还使用上面的建议来计算曼哈顿距离(不是欧几里得),这有助于加快速度。它确实降低了集群的准确性,但同样 - 它几乎不引人注意。现在我们正在快速运行大约 5K 标记,但这个数字越来越大。 曼哈顿距离也很有效——我一直在玩这个。如果您正在处理集群,我认为准确性不是问题。我只是想知道您可能根据 zoomLevel 对 $distance 使用了什么。 大卫,我更新了我的代码以显示我现在如何计算距离。 我已经成功地测试了 10K 标记,方法是添加一个视口查询以将集群限制在视口的范围内。下一站 20K【参考方案8】:

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

【讨论】:

我看到的按经度(或纬度)排序的问题是:当我浏览列表时,我会得到一个区域的所有标记,然后它会跳转到标记在完全不同的区域(但经度稍高)。在列表的下方,我会得到更多应该在第一个区域中的标记,但由于经度高于第二个区域中的标记,所以没有包括在内,但仍然足够接近第一个区域中的标记区域集群。希望这是有道理的。 听起来你的比较一个标记是否与另一个标记相交的算法不正确;我在每个标记点周围创建一个矩形,并测试与排序列表中的下一个矩形的交集。一旦列表中的标记的交集测试失败,列表中的任何其他标记就不可能相交,因为它已排序。这些计算是在像素空间中完成的(我已将 lat/lng 转换为地图上所见的像素)。

以上是关于地图聚类算法的主要内容,如果未能解决你的问题,请参考以下文章

黄钢,瞿伟斌等: 基于改进密度聚类算法的交通事故地点聚类研究

聚类算法 | 景区经纬度数据的抓取和聚类

java 实现DBScan聚类算法

空间聚类算法简述

聚类算法 - kmeans

k均值聚类算法、c均值聚类算法、模糊的c均值聚类算法的区别