Python - 如何加快城市之间距离的计算

时间:2013-12-18 10:00:02

标签: python django algorithm distance

我的数据库中有55249个城市。每一个都有纬度经度值。 对于每个城市,我想计算到每个其他城市的距离,并存储不超过30公里的城市。这是我的算法:

# distance function
from math import sin, cos, sqrt, atan2, radians

def distance(obj1, obj2):
    lat1 = radians(obj1.latitude)
    lon1 = radians(obj1.longitude)
    lat2 = radians(obj2.latitude)
    lon2 = radians(obj2.longitude)
    dlon = lon2 - lon1
    dlat = lat2 - lat1
    a = (sin(dlat/2))**2 + cos(lat1) * cos(lat2) * (sin(dlon/2))**2
    c = 2 * atan2(sqrt(a), sqrt(1-a))
    return round(6373.0 * c, 2)

def distances():
    cities = City.objects.all()  # I am using Django ORM
    for city in cities:
        closest = list()
        for tested_city in cities:
            distance = distance(city, tested_city)
            if distance <= 30. and distance != 0.:
                closest.append(tested_city)
        city.closest_cities.add(*closest)  # again, Django thing
        city.save()  # Django

这可行,但需要花费大量时间。要花上好几周才能完成。我可以用任何方式加快速度吗?

5 个答案:

答案 0 :(得分:7)

你无法计算每对城市之间的距离。相反,您需要将城市放在space-partitioning data structure中,以便进行快速最近邻居查询。 SciPy附带kd-tree实施scipy.spatial.KDTree,适用于此应用。

这里有两个困难。首先,scipy.spatial.KDTree使用点之间的欧几里德距离,但是您想要使用沿地球表面的大圆距离。其次,经度环绕,因此最近的邻居可能有相差360°的经度。如果采取以下方法,这两个问题都可以解决:

  1. 将您的位置从geodetic coordinates纬度经度)转换为ECEF(以地球为中心,地球固定的)坐标( x y z )。

  2. 将这些ECEF坐标放入scipy.spatial.KDTree

  3. 将您的大圆距离(例如,30公里)转换为欧几里德距离。

  4. 致电scipy.spatial.KDTree.query_ball_point以获取范围内的城市。

  5. 以下是一些示例代码来说明这种方法。函数geodetic2ecef来自PySatel by David Parunakian,并根据GPL许可。

    from math import radians, cos, sin, sqrt
    
    # Constants defined by the World Geodetic System 1984 (WGS84)
    A = 6378.137
    B = 6356.7523142
    ESQ = 6.69437999014 * 0.001
    
    def geodetic2ecef(lat, lon, alt=0):
        """Convert geodetic coordinates to ECEF."""
        lat, lon = radians(lat), radians(lon)
        xi = sqrt(1 - ESQ * sin(lat))
        x = (A / xi + alt) * cos(lat) * cos(lon)
        y = (A / xi + alt) * cos(lat) * sin(lon)
        z = (A / xi * (1 - ESQ) + alt) * sin(lat)
        return x, y, z
    
    def euclidean_distance(distance):
        """Return the approximate Euclidean distance corresponding to the
        given great circle distance (in km).
    
        """
        return 2 * A * sin(distance / (2 * B))
    

    让我们组成五万个随机城市位置并将它们转换为ECEF坐标:

    >>> from random import uniform
    >>> cities = [(uniform(-90, 90), uniform(0, 360)) for _ in range(50000)]
    >>> ecef_cities = [geodetic2ecef(lat, lon) for lat, lon in cities]
    

    将它们放入scipy.spatial.KDTree

    >>> import numpy
    >>> from scipy.spatial import KDTree
    >>> tree = KDTree(numpy.array(ecef_cities))
    

    查找伦敦约100公里范围内的所有城市:

    >>> london = geodetic2ecef(51, 0)
    >>> tree.query_ball_point([london], r=euclidean_distance(100))
    array([[37810, 15755, 16276]], dtype=object)
    

    对于您查询的每个点,此数组包含距离r内的城市数组。每个邻居都作为其传递给KDTree的原始数组中的索引。因此,在伦敦约100公里范围内有三个城市,即原始列表中索引为37810,15755和16276的城市:

    >>> from pprint import pprint
    >>> pprint([cities[i] for i in [37810, 15755, 16276]])
    [(51.7186871990946, 359.8043453670437),
     (50.82734317063884, 1.1422052710187103),
     (50.95466110717763, 0.8956257749604779)]
    

    注意:

    1. 您可以从示例输出中看到,正确发现经度相差大约360°的邻居。

    2. 这种方法似乎足够快。在这里,我们发现前1000个城市的30公里范围内的邻居大约需要5秒钟:

      >>> from timeit import timeit
      >>> timeit(lambda:tree.query_ball_point(ecef_cities[:1000], r=euclidean_distance(30)), number=1)
      5.013611573027447
      

      推断,我们希望在大约四分钟内找到所有50,000个城市30公里范围内的邻居。

    3. 我的euclidean_distance函数高估了与给定的大圆距离相对应的欧几里德距离(以免错过任何城市)。对于某些应用程序来说这可能已经足够了 - 毕竟,城市不是点对象 - 但如果你需要比这更精确,那么你可以使用{{3}中的一个大圆距离函数来过滤结果点。 }。

答案 1 :(得分:4)

如果您知道城市距离超过30公里,则可以通过不输入复杂的三角公式来加快距离计算,因为它们的纬度差异对应于超过30公里的弧度。长度a = 30km的弧对应于a / r = 0.00470736的角度,因此:

def distance(obj1, obj2):
    lat1 = radians(obj1.latitude)
    lon1 = radians(obj1.longitude)
    lat2 = radians(obj2.latitude)
    lon2 = radians(obj2.longitude)
    dlon = lon2 - lon1
    dlat = lat2 - lat1

    if dlat > 0.00471:
        return 32

    a = (sin(dlat/2))**2 + cos(lat1) * cos(lat2) * (sin(dlon/2))**2
    c = 2 * atan2(sqrt(a), sqrt(1-a))
    return round(6373.0 * c, 2)

半径32只是一个虚拟值,表示城市相距30公里以上。您应该为经度应用类似的逻辑,您必须考虑最大的绝对纬度:

    if cos(lat1) * dlon > 0.00471 and cos(lat2) * dlon > 0.00471:
        return 32

如果您知道您的城市处于固定的纬度范围内,您可以将恒定限制调整为最差情况。例如,如果您所有的城市都位于连续的美国,则它们应低于纬度49°N,然后您的限制为0.00471 / cos(49°)= 0.00718。

    if dlon > 0.00718:
        return 32

这个更简单的标准意味着您输入的是德克萨斯州或佛罗里达州太多城市的精确计算。您也可以链接这些标准。首先使用近似限制,然后使用基于最大绝对纬度的精确限制,然后计算所有剩余候选者的确切距离。

您可以使用最大绝对纬度预先计算此限制。正如RemcoGerlich所建议的那样,这种启发式方法还可以帮助您将城市置于经度和纬度固定的水库中。他的方法应该通过事先考虑合理的城市对来大大加快你的过程。

编辑我有点惭愧地看到上面的代码没有检查限制的绝对值。无论如何,这里真正的教训是,无论你加快距离计算的速度,大数据集的真正好处来自于选择智能搜索机制,如桶搜索或其他评论者建议的kd树,可能还有一些记忆到清除双重检查。

答案 2 :(得分:3)

我首先创建“扇区”,每个扇区受到相隔X km的2个纬度和相距X km的2个经度的限制。 X应该尽可能大,有一个限制:一个扇区内的所有城市都不超过30公里。

扇区可以存储在一个数组中:

Sector[][] sectors;

在此数组中,可以直接识别包含特定坐标的扇区。识别特定部门的相邻部门也很容易。

然后:

(1)每个城市都有自己的部门。每个部门都有一个城市列表。

(2)对于每个城市,查找其所在城市的所有城市。那些立即符合30公里的标准。

(3)对于每个城市C,找到所有8个相邻区域中的所有城市C'。对于每个C',检查距离C-C'并输出C-C'如果它是&lt; 30公里。

这个算法仍然是O(n ^ 2),但它应该更快,因为对于每个城市,你只检查整个集合的一小部分。

答案 3 :(得分:2)

  1. 尝试不同的算法来加速单人距离的计算,正如有人所说的那样。
  2. 仅使用城市的排列来重新计算重复次数。
  3. 使用multiprocessing模块在​​多个核心上分配工作。
  4. 1和2很简单。 对于第3点,我建议使用imap_unordered()来实现最大速度,使用类似于此的工作流程:

    • 获取所有排列
    • 主循环中的
    • 在单个数据存储区调用中加载所有城市模型
    • 将距离计算分配给工作人员池
    • 尝试将结果保存在单个工作程序或主线程中。我怀疑它在工作者中会更好,但我不知道django将如何在离线脚本中处理并发性(如果有人知道请在评论中加入,以便我们可以集成)。
    • 如果您保存在主线程或单个工作线程中,请尝试使用事务保存大块模型。

    但是

    您还需要稍微改变一下您的模型。对于分布式处理,您需要解耦closest_cities变量。因为不同的流程会改变它。您可以使用主进程级别的列表字典存储任何给定城市的所有最近城市作为密钥,然后将其存储到每个模型,循环结束或同时。

答案 4 :(得分:0)

你正在做很多不必要的工作。

正如其他人所建议的那样,您可以通过更改循环结构来限制计算次数。你有:

for city in cities:
    for tested_city in cities:

因此,您不仅要将每个城市与自身进行比较,还要将city1city2进行比较,之后您会将city2city1进行比较。

我不是Python程序员,所以我不能告诉你这里使用什么语法,但你想要的是一个类似于的嵌套循环结构:

for (i = 0; i < cities.Length-1; ++i)
{
    for (j = i+1; j < cities.Length; ++j)
    {
        compare_cities(cities[i], cities[j]);
    }
}

这将使您需要做的城市比较数量减少一半。这将它从大约30亿的距离计算减少到大约15亿。

其他人也提到了在进入昂贵的三角函数之前比较dlatdlong的早期潜力。

您还可以将lat1lon1转换为弧度一次,并计算cos(lat1)一次并将这些值传递给距离计算,而不是每次计算它们,从而节省一些时间。例如:

for (i = 0; i < cities.Length-1; ++i)
{
    lat1 = radians(cities[i].latitude
    lon1 = radians(cities[i].longitude
    cos1 = cos(lat1)
    for (j = i+1; j < cities.Length; ++j)
    {
        compare_cities(lat1, lon1, cos1, cities[j]);
    }
}

而且您并不需要将c转换为公里。例如,您有:

return round(6373.0 * c, 2)

结果必须是<= 30.0。为什么乘法和舍入?您可以return c,并在代码中将返回值与0.004730.0/6373)进行比较。

相关问题