【问题标题】:Filter zipcodes by proximity in Django with the Spherical Law of Cosines使用余弦球定律在 Django 中按邻近度过滤邮政编码
【发布时间】:2009-12-16 19:12:28
【问题描述】:

我正在尝试处理 Django 中基本商店定位器的邻近搜索。我不想在我的应用程序中使用 PostGIS 以便我可以使用 GeoDjango 的距离过滤器,而是想在模型查询中使用余弦距离公式的球面定律。为了提高效率,我希望在一次查询中在数据库中完成所有计算。

来自 Internet 的一个示例 MySQL 查询,它实现了像这样的球面余弦定律:

SELECT id, ( 
    3959 * acos( cos( radians(37) ) * cos( radians( lat ) ) * 
    cos( radians( lng ) - radians(-122) ) + sin( radians(37) ) * 
    sin( radians( lat ) ) ) 
) 
AS distance FROM stores HAVING distance < 25 ORDER BY distance LIMIT 0 , 20;

查询需要为每个商店的 lat/lng 值引用 Zipcode ForeignKey。如何在 Django 模型查询中完成所有这些工作?

【问题讨论】:

  • (a) 这不是haversine 公式;这是spherical law of cosines 公式;请参阅(例如)http://www.movable-type.co.uk/scripts/latlong.html 并查看相应的 Wikipedia 文章。 (b) 我相信您会用变量替换硬编码的用户坐标 :-) (c) 应该警告温和的读者,您的距离单位有点过时(与 1000 *(罗马长度军团的标准步伐),我相信):-)
  • 天哪,GeoDjango 有什么难的?只需安装它:)

标签: python django zipcode proximity


【解决方案1】:

可以执行raw SQL queries in Django

我的建议是,编写查询以提取 ID 列表(看起来您现在正在这样做),然后使用 ID 提取关联模型(在常规的非原始 SQL Django 查询中) .尽量让你的 SQL 与方言无关,这样如果你不得不切换数据库,你就不必再担心一件事了。

为了澄清,这里有一个如何做的例子:

def get_models_within_25 (self):
    from django.db import connection, transaction
    cursor = connection.cursor()

    cursor.execute("""SELECT id, ( 
        3959 * acos( cos( radians(37) ) * cos( radians( lat ) ) * 
        cos( radians( lng ) - radians(-122) ) + sin( radians(37) ) * 
        sin( radians( lat ) ) ) )
        AS distance FROM stores HAVING distance < 25
        ORDER BY distance LIMIT 0 , 20;""")
    ids = [row[0] for row in cursor.fetchall()]

    return MyModel.filter(id__in=ids)

作为免责声明,我不能保证这段代码,因为我已经有几个月没有编写任何 Django,但它应该是正确的。

【讨论】:

  • 它工作得很好,只需要三引号(或变成一个字符串)。
  • 只是对此的跟进。原始查询返回一个“距离”字段(显示 2 组 log/lat 之间的距离)。如果使用这个额外的“距离”字段,我将如何执行第二部分?
  • 我假设 "ids = [row[0] for row in cursor.fetchall()] return MyModel.filter(id__in=ids)" 从第一个查询中获取所有 IDS,然后使用 djangos ORM 从模型中选择所有行?
【解决方案2】:

为了跟进 Tom 的回答,默认情况下它不会在 SQLite 中工作,因为 SQLite 默认缺少数学函数。没问题,添加起来很简单:

class LocationManager(models.Manager):
    def nearby_locations(self, latitude, longitude, radius, max_results=100, use_miles=True):
        if use_miles:
            distance_unit = 3959
        else:
            distance_unit = 6371

        from django.db import connection, transaction
        from mysite import settings
        cursor = connection.cursor()
        if settings.DATABASE_ENGINE == 'sqlite3':
            connection.connection.create_function('acos', 1, math.acos)
            connection.connection.create_function('cos', 1, math.cos)
            connection.connection.create_function('radians', 1, math.radians)
            connection.connection.create_function('sin', 1, math.sin)

        sql = """SELECT id, (%f * acos( cos( radians(%f) ) * cos( radians( latitude ) ) *
        cos( radians( longitude ) - radians(%f) ) + sin( radians(%f) ) * sin( radians( latitude ) ) ) )
        AS distance FROM location_location WHERE distance < %d
        ORDER BY distance LIMIT 0 , %d;""" % (distance_unit, latitude, longitude, latitude, int(radius), max_results)
        cursor.execute(sql)
        ids = [row[0] for row in cursor.fetchall()]

        return self.filter(id__in=ids)

【讨论】:

  • 看起来很神奇,它是如何工作的?估计效率很低吧?
  • “from mysite import settings”可以通过使用“from django.conf import settings”变得更通用
【解决方案3】:

为了跟进 Tom,如果你想要一个也适用于 postgresql 的查询,你不能使用 AS,因为你会得到一个错误,说“距离”不存在。

你应该把整个球面定律表达式放在 WHERE 子句中,像这样(它也适用于 mysql):

import math
from django.db import connection, transaction
from django.conf import settings

from django .db import models

class LocationManager(models.Manager):
    def nearby_locations(self, latitude, longitude, radius, use_miles=False):
        if use_miles:
            distance_unit = 3959
        else:
            distance_unit = 6371

        cursor = connection.cursor()

        sql = """SELECT id, latitude, longitude FROM locations_location WHERE (%f * acos( cos( radians(%f) ) * cos( radians( latitude ) ) *
            cos( radians( longitude ) - radians(%f) ) + sin( radians(%f) ) * sin( radians( latitude ) ) ) ) < %d
            """ % (distance_unit, latitude, longitude, latitude, int(radius))
        cursor.execute(sql)
        ids = [row[0] for row in cursor.fetchall()]

        return self.filter(id__in=ids)

请注意必须选择经纬度,否则不能在WHERE子句中使用。

【讨论】:

    【解决方案4】:

    只是为了跟进 jboxer 的回答,这是作为自定义管理器一部分的整个事情,其中​​一些硬编码的东西变成了变量:

    class LocationManager(models.Manager):
        def nearby_locations(self, latitude, longitude, radius, max_results=100, use_miles=True):
            if use_miles:
                distance_unit = 3959
            else:
                distance_unit = 6371
    
            from django.db import connection, transaction
            cursor = connection.cursor()
    
            sql = """SELECT id, (%f * acos( cos( radians(%f) ) * cos( radians( latitude ) ) *
            cos( radians( longitude ) - radians(%f) ) + sin( radians(%f) ) * sin( radians( latitude ) ) ) )
            AS distance FROM locations_location HAVING distance < %d
            ORDER BY distance LIMIT 0 , %d;""" % (distance_unit, latitude, longitude, latitude, int(radius), max_results)
            cursor.execute(sql)
            ids = [row[0] for row in cursor.fetchall()]
    
            return self.filter(id__in=ids)
    

    【讨论】:

      【解决方案5】:

      根据 jboxer 的回复

      def find_cars_within_miles_from_postcode(request, miles, postcode=0):
      
          # create cursor for RAW query
          cursor = connection.cursor()
      
          # Get lat and lon from google
          lat, lon = getLonLatFromPostcode(postcode)
      
          # Gen query
          query = "SELECT id, ((ACOS(SIN("+lat+" * PI() / 180) * SIN(lat * PI() / 180) + COS("+lat+" * PI() / 180) * COS(lat * PI() / 180) * COS(("+lon+" - lon) * PI() / 180)) * 180 / PI()) * 60 * 1.1515) AS distance FROM app_car HAVING distance<='"+miles+"' ORDER BY distance ASC"
      
          # execute the query
          cursor.execute(query)
      
          # grab all the IDS form the sql result
          ids = [row[0] for row in cursor.fetchall()]
      
          # find cars from ids
          cars = Car.objects.filter(id__in=ids)
      
          # return the Cars with these IDS
          return HttpResponse( cars )
      

      这会从 x 英里数返回我的汽车,这很好用。但是原始查询返回了它们与某个位置的距离,我认为字段名称是“距离”。

      如何使用我的汽车对象返回此字段“距离”?

      【讨论】:

        【解决方案6】:

        使用上面提出的一些答案,我得到的结果不一致,所以我决定再次检查方程 使用 [此链接]http://www.movable-type.co.uk/scripts/latlong.html 作为参考,等式是 d = acos(sin(lat1)*sin(lat2) + cos(lat1)*cos(lat2)*cos(lon2-lon1) ) * 6371 其中d 是要计算的距离,

        lat1,lon1 是基点的坐标,lat2,lon2 是其他点的坐标,在我们的例子中是数据库中的点。

        从上面的答案来看,LocationManager 类看起来像这样

        class LocationManager(models.Manager):
        def nearby_locations(self, latitude, longitude, radius, max_results=100, use_miles=True):
            if use_miles:
                distance_unit = 3959
            else:
                distance_unit = 6371
        
            from django.db import connection, transaction
            from mysite import settings
            cursor = connection.cursor()
            if settings.DATABASE_ENGINE == 'sqlite3':
                connection.connection.create_function('acos', 1, math.acos)
                connection.connection.create_function('cos', 1, math.cos)
                connection.connection.create_function('radians', 1, math.radians)
                connection.connection.create_function('sin', 1, math.sin)
        
            sql = """SELECT id, (acos(sin(radians(%f)) * sin(radians(latitude)) + cos(radians(%f))
                  * cos(radians(latitude)) * cos(radians(%f-longitude))) * %d)
            AS distance FROM skills_coveragearea WHERE distance < %f
            ORDER BY distance LIMIT 0 , %d;""" % (latitude, latitude, longitude,distance_unit, radius, max_results)
            cursor.execute(sql)
            ids = [row[0] for row in cursor.fetchall()]
        
            return self.filter(id__in=ids)
        

        使用站点 [link]http://www.movable-type.co.uk/scripts/latlong.html 作为检查,我的结果一致。

        【讨论】:

          【解决方案7】:

          也可以使用 Django 的数据库函数来执行此操作,这意味着您可以使用 .annotate() 调用添加 distance_miles 列,然后按它排序。这是一个例子:

          from django.db.models import F
          from django.db.models.functions import ACos, Cos, Radians, Sin
          
          locations = Location.objects.annotate(
              distance_miles = ACos(
                  Cos(
                      Radians(input_latitude)
                  ) * Cos(
                      Radians(F('latitude'))
                  ) * Cos(
                      Radians(F('longitude')) - Radians(input_longitude)
                  ) + Sin(
                      Radians(input_latitude)
                  ) * Sin(Radians(F('latitude')))
              ) * 3959
          ).order_by('distance_miles')[:10]
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2011-09-13
            • 1970-01-01
            • 1970-01-01
            • 2011-06-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多