【问题标题】:Calculate distance between Zip Codes... AND users.计算邮政编码...和用户之间的距离。
【发布时间】:2011-04-28 08:29:25
【问题描述】:

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

我在 2000 年左右建立了一个约会网站(早已不复存在),其中一个挑战是计算用户之间的距离,以便我们可以在 X 英里半径范围内展示您的“匹配”。仅说明问题,给定以下数据库架构(大致):

用户表 用户身份 用户名 邮政编码

邮政编码表 邮政编码 纬度 经度

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

您会采用什么方法来回答以下问题:哪些其他用户居住在位于指定用户邮政编码 X 英里范围内的邮政编码中。

我们使用了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 表中会有很多行。它不是完全不可行,但它确实很大。此外,它还需要对整个数据集进行完整的前期工作,这也不是不可管理的,但不一定是可取的。

无论如何,我想知道你们中的一些专家可能会采取什么方法来处理这样的事情。此外,我认为这是程序员必须不时解决的一个常见问题,尤其是当您考虑算法相似的问题时。我对一个彻底的解决方案感兴趣,该解决方案至少包括所有部分的提示,以便真正快速有效地结束。谢谢!

【问题讨论】:

    标签: database gis geocoding distance zipcode


    【解决方案1】:

    好的,对于初学者,您实际上并不需要在这里使用 Haversine 公式。对于不太准确的公式会产生较大误差的大距离,您的用户不在乎匹配是正负几英里,而对于更近的距离,误差非常小。 Geographical Distance Wikipedia 文章中列出了更容易(计算)的公式。

    由于邮政编码不像均匀分布的那样,任何均匀划分它们的过程都会在它们紧密聚集的区域中遭受巨大的损失(华盛顿附近的东海岸就是一个很好的例子)。如果您想要直观比较,请查看http://benfry.com/zipdecode 并将邮政编码前缀 89 与 07 进行比较。

    处理索引这个空间的更好方法是使用像QuadtreeR-tree 这样的数据结构。这种结构允许您对不均匀分布的数据进行空间和距离搜索。

    这是四叉树的样子:

    要对其进行搜索,您可以使用其中的较小单元格的索引向下钻取每个较大的单元格。维基百科解释得更透彻。

    当然,由于这是相当普遍的事情,其他人已经为您完成了困难的部分。由于您尚未指定您使用的数据库,因此 PostgreSQL 扩展 PostGIS 将作为示例。 PostGIS 包含执行 R-tree 空间索引的功能,可让您进行高效的空间查询。

    导入数据并构建空间索引后,查询距离就是这样的查询:

    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
    

    我会让你自己完成本教程的其余部分。

    这里有一些其他参考资料可以帮助您入门。

    【讨论】:

    • 保罗,我不知道我会得到这么多和不同的回答这个问题。我发现你的内容特别丰富。感谢您抽出宝贵的时间提供如此详尽的解释。
    • 谢谢!很开心你喜欢。当我开始回答这个问题时,确实花费的时间比我最初预期的要长,但我想这就是 SO 的工作原理! ;)
    【解决方案2】:

    我只需创建一个 zip_code_distances 表并预先计算美国所有 42K 邮政编码之间的距离,这些邮政编码彼此之间的半径在 20-25 英里之间。

    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;
    

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

    我从网上下载了一个邮政编码数据文件,其中包含 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
    ...
    

    我编写了一个快速而肮脏的 C# 程序来读取文件并计算每个邮政编码之间的距离,但只输出 25 英里半径内的邮政编码:

    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 将这个距离数据加载到我的 zip_code_distances 表中,然后使用它来限制我的应用程序的搜索空间。

    例如,如果您有一个邮政编码为 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 英里,这将邮政编码距离的数量增加到 3250 万行。

    邮政编码 91210 运行时 0.009 秒的快速性能检查。

    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
    

    【讨论】:

    • 这是一个很好的解决方案,但需要假设给定距离。当然,我上面喜欢的解决方案在查询时会随着距离的增加而增长,而这个解决方案不会。但是,如果您使用 100 英里作为外部限制,您会结束多少行?如果不运行它,我不确定答案是什么,但我怀疑它比 4M 大得多。我也不确定 SQL Server 中行的实际限制是多少,但我怀疑我会靠运气来保持这么多行的出色性能。
    • 这是一个 1.25 亿行的表示例,它查询 340K 行,但将结果限制为 32 行,它使用 innodb 并利用聚集主键索引作为我上面的示例 stackoverflow.com/questions/3534597/…。运行时间为 0.02 秒。
    • 半径为 100 英里,有 9500 万行,这在方案中仍然相当琐碎。我将编辑我的帖子并进行一些性能测试供您查看。
    • 上面的评论应该是 3300 万而不是 95 正在查看错误的数据 - 哎呀。
    • 从那以后,我对大型 SQL 表的性能限制有了很多了解。它们并不像我想象的那么慢。谢谢,伙计们,你们付出了所有额外的努力。您的回答和 cmets 非常有帮助。
    【解决方案3】:

    您可以通过假设一个盒子而不是圆形半径来简化计算。然后,在搜索时,您只需计算给定点+“半径”的纬度/经度的下限/上限,只要您在纬度/经度列上有索引,您就可以很容易地拉回所有落在框中的记录.

    【讨论】:

      【解决方案4】:

      我知道这篇文章太旧了,但是为客户做一些研究我发现了 Google Maps API 的一些有用功能并且实现起来非常简单,您只需将起点和终点 ZIP 传递给 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

      点击链接,您可以看到它返回了一个 json。请记住,您需要一个 API 密钥才能在您自己的主机上使用它。

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

      【讨论】:

        【解决方案5】:

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

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

        它在数学上肯定更复杂,特别是必须选择区域数量,以便在表格大小与动态计算所花费的时间之间取得良好平衡,但它会减少预先计算的表格的大小有很大的优势。

        【讨论】:

        • 这似乎是完成一些索引的真正快速方法,但索引数据集要小得多(因此更有用)。这可能比我在下面发布的解决方案更快。我说可能是因为我还没有考虑清楚。我怀疑这种变化可用于获取已知在范围内的邮政编码,并允许我按纬度和经度进行盒装选择,然后使用 Haversinse 公式计算更小的距离数。
        • 邮政编码的大小并不大致相等。我认为有更好的解决方案来进行这种空间分解。
        • 例如,将邮政编码前缀 89* 与 07* 进行比较。这里的可视化效果很好:benfry.com/zipdecode
        • @Paul:嗯!实际上,在我的脑海中,我将邮政编码视为点,但这也适用于成形的邮政编码。我的答案中大致相同大小的“区域”意味着每个(或其中的一部分)包含几个邮政编码。这个想法是对明显在范围内或明显超出范围的区域进行快速、粗略的清除,因此您只能在必要时处理复杂、众多(现在是可变大小的!)邮政编码区域。
        • 是的。对于邮政编码,最大数据集大小意味着您的方法可能存在合理的平衡,因为数据集是有限的。 Quadtrees 或 R-trees 是“正确”的方法...... ;)
        【解决方案6】:

        我会使用纬度和经度。例如,如果您的纬度为 45,经度为 45,并被要求在 50 英里范围内查找匹配项,那么您可以通过将纬度上移 50/69 和纬度下移 50/69(1 度纬度 ~ 69 英里)。选择纬度在此范围内的邮政编码。经度略有不同,因为随着您靠近两极,它们会变小。

        但是在 45 度,1 经度 ~ 49 英里处,因此您可以将纬度向左移动 50/49,纬度向右移动 50/49,并从具有该经度的纬度集中选择所有邮政编码。这将为您提供长度为一百英里的正方形内的所有邮政编码。如果你想非常精确,你可以使用你提到的Haversine公式去除盒子角落的拉链,给你一个球体。

        【讨论】:

          【解决方案7】:

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

          我不知道距离计算的复杂性,所以我还会检查动态计算是否比查找更便宜(同时考虑到你必须计算的频率)。

          【讨论】:

          • 关于不计算所有距离的一点是一个很好的观点。我怀疑它最终还是会变得相当大,比如说 100 英里。这基本上是达拉斯和休斯顿之间的一个圈子,约会网站上的人们肯定会开车那么远去认识彼此。但是,我想我会试一试,看看它是否有所改进。
          【解决方案8】:

          我的问题运行良好,几乎每个人的答案都被使用了。我是根据旧的解决方案来考虑这个问题,而不仅仅是“重新开始”。 Babtek 以最简单的方式表达而受到认可。

          我将跳过代码,因为我将提供参考来推导所需的公式,而且这里有太多内容无法清晰地发布。

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

          2) 从 ZipCode 表中选择框内的所有点。这包括一个简单的 WHERE 子句,其中包含两个由 Lat 和 Long 限制的 Between 语句。

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

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

          5) 选择 ZipCode 在剩余点 B 中的用户。

          这对于 > 100 英里来说非常快。最长的结果是大约 0.014 秒来计算匹配,并且运行 select 语句很简单。

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

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

          另外,我有兴趣使用 SQL 的内置地理类型来查看这是否会使这更容易,但旧的 int/float 类型可以很好地用于此示例。

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

          1) Maximum Difference, Latitude and Longitude.

          2) The Haversine Formula.

          3) Lengthy but complete discussion of the whole process,这是我在您的答案中通过谷歌搜索找到的。

          【讨论】:

          • 您不需要存储 16 亿个邮政编码/距离,因为您只对彼此指定半径
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2017-10-25
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-08-12
          • 1970-01-01
          • 2014-06-09
          相关资源
          最近更新 更多