代码之家  ›  专栏  ›  技术社区  ›  bopapa_1979

计算邮政编码…和用户之间的距离。

  •  30
  • bopapa_1979  · 技术社区  · 14 年前

    这是一个比我迫切需要的问题更具挑战性的问题,所以不要整天花在it上。

    我在2000年左右建立了一个约会网站(很久以前),其中一个挑战是计算用户之间的距离,这样我们就可以在X英里半径内展示你的“火柴”。要说明问题,请给出以下数据库架构(大致):

    用户ID 用户名

    ZIPCODE表 邮政编码 纬度 经度

    在USER.ZIPCODE=ZIPCODE.ZIPCODE上加入USER和ZIPCODE。

    我们用了 2000 census data ,其中包含邮政编码表及其近似的格度和经度。

    Haversine Formula 计算球体上任意两点之间的距离。。。数学真的很简单。

    这个问题,至少对我们这些19岁的大学生来说,真的变成了如何有效地计算和/存储从所有成员到所有其他成员的距离。一种方法(我们使用的方法)是导入所有数据并计算从每个邮政编码到每个其他邮政编码的距离。然后将结果存储并索引。类似于:

    SELECT  User.UserId
    FROM    ZipCode AS MyZipCode
            INNER JOIN ZipDistance ON MyZipCode.ZipCode = ZipDistance.MyZipCode
            INNER JOIN ZipCode AS TheirZipCode ON ZipDistance.OtherZipCode = TheirZipCode.ZipCode
            INNER JOIN User AS User ON TheirZipCode.ZipCode = User.ZipCode
    WHERE   ( MyZipCode.ZipCode = 75044 )
            AND ( ZipDistance.Distance < 50 )
    

    当然,问题是ZipDistance表中会有很多行。它不是完全不可行,但它确实很大。它还需要对整个数据集进行完整的预处理,这也不是不可管理的,但不一定是可取的。

    不管怎样,我想知道你们中的一些大师可能会采取什么样的方法。另外,我认为这是程序员经常要解决的一个常见问题,特别是如果你考虑的问题只是算法上相似的话。我感兴趣的是一个彻底的解决方案,其中至少包括所有的部分的提示,这样做真的很快有效结束。谢谢!

    8 回复  |  直到 7 年前
        1
  •  33
  •   bopapa_1979    8 年前

    好吧,首先,你不需要在这里使用哈弗辛公式。对于精度较低的公式会产生较大误差的大距离,用户不在乎匹配是正是负几英里,而对于较近的距离,误差非常小。在 Geographical Distance 维基百科文章。

    由于邮政编码并不像等距编码那样,在它们密集分布的地区,任何对它们进行等距分割的过程都会受到很大的影响(例如,华盛顿附近的东海岸)。如果你想进行视觉比较,请查看 http://benfry.com/zipdecode

    处理这个空间索引的一个更好的方法是使用像 Quadtree 或者 R-tree . 此结构允许您对间距不均匀的数据进行空间和距离搜索。

    Quadtree

    要搜索它,可以使用它内部的较小单元格的索引向下钻取每个较大的单元格。维基百科解释得更透彻。

    PostGIS 将作为一个例子。PostGIS包含了创建R树空间索引的功能,这些索引允许您进行高效的空间查询。

    导入数据并构建空间索引后,查询距离是一个查询,如下所示:

    SELECT zip
    FROM zipcode
    WHERE
    geom && expand(transform(PointFromText('POINT(-116.768347 33.911404)', 4269),32661), 16093)
    AND
    distance(
       transform(PointFromText('POINT(-116.768347 33.911404)', 4269),32661),
       geom) < 16093
    

    我会让你自己完成剩下的教程。

    这里有一些其他的参考资料,让你开始。

        2
  •  14
  •   Jon Black    14 年前

    create table zip_code_distances
    (
    from_zip_code mediumint not null,
    to_zip_code mediumint not null,
    distance decimal(6,2) default 0.0,
    primary key (from_zip_code, to_zip_code),
    key (to_zip_code)
    )
    engine=innodb;
    

    仅将zipcodes包含在彼此半径为20-25英里的范围内,就可以将需要存储在距离表中的行数从最大值17亿(42K^2)-42K减少到更易于管理的400万左右。

    我从网上下载了一个zipcode数据文件,其中包含了所有官方美国zipcodes的csv格式的经纬度:

    "00601","Adjuntas","Adjuntas","Puerto Rico","PR","787","Atlantic", 18.166, -66.7236
    "00602","Aguada","Aguada","Puerto Rico","PR","787","Atlantic", 18.383, -67.1866
    ...
    "91210","Glendale","Los Angeles","California","CA","818","Pacific", 34.1419, -118.261
    "91214","La Crescenta","Los Angeles","California","CA","818","Pacific", 34.2325, -118.246
    "91221","Glendale","Los Angeles","California","CA","818","Pacific", 34.1653, -118.289
    ...
    

    sw = new StreamWriter(path);
    
    foreach (ZipCode fromZip in zips){
    
        foreach (ZipCode toZip in zips)
        {
            if (toZip.ZipArea == fromZip.ZipArea) continue;
    
            double dist = ZipCode.GetDistance(fromZip, toZip);
    
            if (dist > 25) continue;
    
            string s = string.Format("{0}|{1}|{2}", fromZip.ZipArea, toZip.ZipArea, dist);
            sw.WriteLine(s);
        }
    }
    

    结果输出文件如下所示:

    from_zip_code|to_zip_code|distance
    ...
    00601|00606|16.7042215574185
    00601|00611|9.70353520976393
    00601|00612|21.0815707704904
    00601|00613|21.1780461311929
    00601|00614|20.101431539283
    ...
    91210|90001|11.6815708119899
    91210|90002|13.3915723402714
    91210|90003|12.371251171873
    91210|90004|5.26634939906721
    91210|90005|6.56649623829871
    ...
    

    然后,我将使用load data infile将这些距离数据加载到我的邮政编码距离表中,然后使用它来限制我的应用程序的搜索空间。

    例如,如果您有一个用户的zipcode是91210,并且他们希望找到半径在10英里以内的人,那么您现在只需执行以下操作:

    select 
     p.*
    from
     people p
    inner join
    (
     select 
      to_zip_code 
     from 
      zip_code_distances 
     where 
      from_zip_code = 91210 and distance <= 10
    ) search
    on p.zip_code = search.to_zip_code
    where
     p.gender = 'F'....
    

    希望这有帮助

    编辑:将半径扩展到100英里,将zipcode距离的数量增加到3250万行。

    select count(*) from zip_code_distances
    count(*)
    ========
    32589820
    
    select 
     to_zip_code 
    from 
     zip_code_distances 
    where 
     from_zip_code = 91210 and distance <= 10;
    
    0:00:00.009: Query OK
    
        3
  •  5
  •   babtek    14 年前

    可以通过假设一个长方体而不是圆形半径来简化计算。然后,在搜索时,您只需计算给定点的lat/lon的下限/上限+“半径”,只要在lat/lon列上有一个索引,您就可以非常容易地拉回到框中的所有记录。

        4
  •  1
  •   Jander    14 年前

    你可以把你的空间划分成大小大致相等的区域——例如,把地球近似成一个buckyball或二十面体。如果更容易的话,这些区域甚至可以重叠一点(例如,使它们成为圆形)。记录每个邮政编码所在的地区。然后,可以预先计算每个区域对之间可能的最大距离,该区域对具有相同的 计算所有邮政编码对的问题,但对于较小的 n个 .

    现在,对于任何给定的邮政编码,都可以得到一个绝对在给定范围内的区域列表,以及一个跨越边界的区域列表。对于前者,只需获取所有的邮政编码。对于后者,深入到每个边界区域并根据各个邮政编码进行计算。

    从数学上来说,这当然更为复杂,尤其是为了在表的大小与运行时计算时间之间保持良好的平衡,必须选择区域的数量,但它将预先计算的表的大小减少了很好的幅度。

        5
  •  1
  •   halfer    7 年前

    我会用经纬度。例如,如果您的纬度为45,经度为45,并被要求在50英里内找到匹配项,那么您可以通过向上移动纬度的50/69,向下移动纬度的50/69(纬度1度~69英里)来完成此操作。选择纬度在此范围内的邮政编码。经度有点不同,因为当你靠近两极时它们会变小。

    但是在45度,1经度~49英里,所以你可以在纬度上向左移动50/49,在纬度上向右移动50/49,然后从这个经度设置的纬度中选择所有邮政编码。这将为您提供100英里长的正方形内的所有邮政编码。如果你真的想精确一点,你可以用你提到的哈弗森公式来剔除盒子角落里的拉链,给你一个球体。

        6
  •  0
  •   John Smith    14 年前

    不是所有可能的邮政编码都会被使用。我会将zipdistance构建为一个“缓存”表。对于每个请求,计算该对的距离并将其保存在缓存中。当请求距离对时,首先查看缓存,然后计算是否不可用。

    我不知道距离计算的复杂性,所以我也会检查飞行计算是否比查找便宜(还要考虑计算的频率)。

        7
  •  0
  •   Facundo Colombier    9 年前

    我知道这篇文章太老了,但是我为一个客户做了一些研究,我发现了Google Maps API的一些有用的功能,而且实现起来非常简单,你只需要把来源和目的地的邮政编码传给url,它就可以计算出即使是在流量情况下的距离,你可以用任何语言:

    origins = 90210
    destinations = 93030
    mode = driving
    

    http://maps.googleapis.com/maps/api/distancematrix/json?origins=90210&destinations=93030&mode=driving&language=en-EN&sensor=false%22

    资料来源: http://stanhub.com/find-distance-between-two-postcodes-zipcodes-driving-time-in-current-traffic-using-google-maps-api/

        8
  •  0
  •   halfer    7 年前

    我的问题是运行良好,几乎每个人的答案都得到了利用。我是从旧的解决方案的角度来考虑这个问题,而不是仅仅“重新开始”。Babtek因为用最简单的术语来陈述而得到了认可。

    我将跳过这段代码,因为我将提供引用以派生所需的公式,而且这里有太多的内容要干净地发布。

    1) 考虑球面上的点A,用经纬度表示。 Figure out North, South, East, and West edges of a box 2X miles across with Point A at the center .

    3) 使用haversine公式确定点A和步骤2中返回的每个点B之间的球面距离。

    4) 丢弃距离A->B>X处的所有点B。

    5) 选择ZipCode位于其余点集中B的用户。

    这是相当快的100英里。计算匹配的最长结果是~0.014秒,运行select语句的最长结果是微不足道的。

    另外,作为补充说明,有必要在几个函数中实现数学并在SQL中调用它们。一旦超过一定距离,ZipCode的匹配数就太大,无法传递回SQL并用作IN语句,因此我必须使用一个临时表,并将生成的ZipCode连接到ZipCode列上的User。

    我怀疑使用ZipDistance表不会带来长期的性能提升。行数变得非常大。如果计算每个邮政编码到其他邮政编码的距离(最终),那么40000个邮政编码的结果行数将是~1.6B。哇!

    另外,我对使用SQL内置的geography类型感兴趣,看看这是否会使操作更简单,但是对于这个示例来说,好的旧int/float类型很好。

    所以。。。我使用的在线资源的最终列表,供您参考:

    (一) Maximum Difference, Latitude and Longitude

    2个) The Haversine Formula .

    三) Lengthy but complete discussion of the whole process