加速处理500万行坐标数据

时间:2016-03-14 02:00:40

标签: python csv geopy

我有一个包含两列(纬度,经度)的csv文件,其中包含超过500万行的地理位置数据。 我需要确定与列表中任何其他点不在5英里范围内的点,并将所有内容输出回另一个具有额外列(CloseToAnotherPoint)的CSV,如果有另一列True积分在5英里以内,False如果没有。

以下是我使用geopy的当前解决方案(不进行任何网络调用,仅使用函数计算距离):

from geopy.point import Point
from geopy.distance import vincenty
import csv


class CustomGeoPoint(object):
    def __init__(self, latitude, longitude):
        self.location = Point(latitude, longitude)
        self.close_to_another_point = False


try:
    output = open('output.csv','w')
    writer = csv.writer(output, delimiter = ',', quoting=csv.QUOTE_ALL)
    writer.writerow(['Latitude', 'Longitude', 'CloseToAnotherPoint'])

    # 5 miles
    close_limit = 5
    geo_points = []

    with open('geo_input.csv', newline='') as geo_csv:
        reader = csv.reader(geo_csv)
        next(reader, None) # skip the headers
        for row in reader:
            geo_points.append(CustomGeoPoint(row[0], row[1]))

    # for every point, look at every point until one is found within 5 miles
    for geo_point in geo_points:
        for geo_point2 in geo_points:
            dist = vincenty(geo_point.location, geo_point2.location).miles
            if 0 < dist <= close_limit: # (0,close_limit]
                geo_point.close_to_another_point = True
                break
        writer.writerow([geo_point.location.latitude, geo_point.location.longitude,
                         geo_point.close_to_another_point])

finally:
    output.close()

正如您可能从中看到的那样,这种解决方案非常缓慢。事实上,我让它运行 3天这么慢,但它仍然没有完成!

我已经考虑过将数据拆分成块(多个CSV文件或其他东西),这样内部循环就不必查看其他每一点,但是我必须要考虑如何确保每个部分的边界检查其相邻部分的边界,这看起来过于复杂,我担心这将是一件令人头疼的事情,而不是它的价值。

那么关于如何加快这一点的任何指示?

7 个答案:

答案 0 :(得分:3)

正如Python calculate lots of distances quickly的答案所指出的,这是k-D树的经典用例。

另一种方法是使用扫描线算法,如How do I match similar coordinates using Python?

的答案所示

这里是适合您的问题的扫描线算法。在我的笔记本电脑上,它需要&lt; 5分钟可以通过5M随机点。

import itertools as it
import operator as op
import sortedcontainers     # handy library on Pypi
import time

from collections import namedtuple
from math import cos, degrees, pi, radians, sqrt
from random import sample, uniform

Point = namedtuple("Point", "lat long has_close_neighbor")

miles_per_degree = 69

number_of_points = 5000000
data = [Point(uniform( -88.0,  88.0),     # lat
              uniform(-180.0, 180.0),     # long
              True
             )
        for _ in range(number_of_points)
       ]

start = time.time()
# Note: lat is first in Point, so data is sorted by .lat then .long.
data.sort()

print(time.time() - start)

# Parameter that determines the size of a sliding lattitude window
# and therefore how close two points need to be to be to get flagged.
threshold = 5.0  # miles
lat_span = threshold / miles_per_degree
coarse_threshold = (.98 * threshold)**2

# Sliding lattitude window.  Within the window, observations are
# ordered by longitude.
window = sortedcontainers.SortedListWithKey(key=op.attrgetter('long'))

# lag_pt is the 'southernmost' point within the sliding window.
point = iter(data)
lag_pt = next(point)

milepost = len(data)//10

# lead_pt is the 'northernmost' point in the sliding window.
for i, lead_pt in enumerate(data):
    if i == milepost:
        print('.', end=' ')
        milepost += len(data)//10

    # Dec of lead_obs represents the leading edge of window.
    window.add(lead_pt)

    # Remove observations further than the trailing edge of window.
    while lead_pt.lat - lag_pt.lat > lat_span:
        window.discard(lag_pt)
        lag_pt = next(point)

    # Calculate 'east-west' width of window_size at dec of lead_obs
    long_span = lat_span / cos(radians(lead_pt.lat))
    east_long = lead_pt.long + long_span
    west_long = lead_pt.long - long_span

    # Check all observations in the sliding window within
    # long_span of lead_pt.
    for other_pt in window.irange_key(west_long, east_long):

        if other_pt != lead_pt:
            # lead_pt is at the top center of a box 2 * long_span wide by 
            # 1 * long_span tall.  other_pt is is in that box. If desired, 
            # put additional fine-grained 'closeness' tests here. 

            # coarse check if any pts within 80% of threshold distance
            # then don't need to check distance to any more neighbors
            average_lat = (other_pt.lat + lead_pt.lat) / 2
            delta_lat   = other_pt.lat - lead_pt.lat
            delta_long  = (other_pt.long - lead_pt.long)/cos(radians(average_lat))

            if delta_lat**2 + delta_long**2 <= coarse_threshold:
                break

            # put vincenty test here
            #if 0 < vincenty(lead_pt, other_pt).miles <= close_limit:
            #    break

    else:
        data[i] = data[i]._replace(has_close_neighbor=False)

print()      
print(time.time() - start)

答案 1 :(得分:2)

如果按纬度(n log(n))对列表进行排序,并且点大致均匀分布,则每个点5英里内将其降低到大约1000点(餐巾纸数学,不精确)。通过仅查看纬度附近的点,运行时从n ^ 2变为n * log(n)+。0004n ^ 2。希望这能加快它的速度。

答案 2 :(得分:1)

我会分三步重做算法:

  1. 使用大圆距离,假设误差为1%,因此限制等于1.01 *限制。

  2. 将大圆距离编码为内联函数,此测试应该快速

  3. 你会得到一些误报,你可以用vincenty进一步测试

答案 3 :(得分:1)

我会试试pandas。 Pandas用于高效处理大量数据。无论如何,这可能有助于提高csv部分的效率。但是从它的声音来看,你已经找到了一个固有的低效问题需要解决。你拿第1点并将它与4,999,999个其他点进行比较。然后你拿第2点并将它与4,999,998个其他点进行比较,依此类推。算一算。这是您正在进行的12.5万亿比较。如果你每秒可以进行1,000,000次比较,那就是144天的计算。如果你每秒可以做10,000,000次比较,那就是14天。对于直接python中的添加,10,000,000次操作可能需要1.1秒,但我怀疑你的比较与添加操作一样快。所以至少要两周或两周。

或者,您可以提出一种替代算法,但我没有考虑任何特定的算法。

答案 4 :(得分:1)

这只是第一次通过,但到目前为止我使用great_circle()代替vincinty()加快了一半,并清理了其他一些事情。区别在于here,准确度的损失约为0.17%

from geopy.point import Point
from geopy.distance import great_circle
import csv


class CustomGeoPoint(Point):
    def __init__(self, latitude, longitude):
        super(CustomGeoPoint, self).__init__(latitude, longitude)
        self.close_to_another_point = False


def isCloseToAnother(pointA, points):
    for pointB in points:
        dist = great_circle(pointA, pointB).miles
        if 0 < dist <= CLOSE_LIMIT:  # (0, close_limit]
            return True

    return False


    with open('geo_input.csv', 'r') as geo_csv:
        reader = csv.reader(geo_csv)
        next(reader, None)  # skip the headers

        geo_points = sorted(map(lambda x: CustomGeoPoint(x[0], x[1]), reader))

    with open('output.csv', 'w') as output:
        writer = csv.writer(output, delimiter=',', quoting=csv.QUOTE_ALL)
        writer.writerow(['Latitude', 'Longitude', 'CloseToAnotherPoint'])

        # for every point, look at every point until one is found within a mile
        for point in geo_points:
            point.close_to_another_point = isCloseToAnother(point, geo_points)
            writer.writerow([point.latitude, point.longitude,
                             point.close_to_another_point])

我将进一步改进这一点。

在:

$ time python geo.py

real    0m5.765s
user    0m5.675s
sys     0m0.048s

后:

$ time python geo.py

real    0m2.816s
user    0m2.716s
sys     0m0.041s

答案 5 :(得分:1)

奥斯卡史密斯提出的更好的解决方案。你有一个csv文件,只是在excel中排序它是非常有效的)。然后在您的程序中使用二进制搜索来查找5英里范围内的城市(您可以对二进制搜索方法进行小的更改,以便在找到满足您条件的一个城市时会中断)。 另一个改进是当你发现一个城市在另一个城市之内时,设置地图以记住这对城市。例如,当您发现城市A在距离城市B 5英里范围内时,请使用地图存储该对(B是键,A是值)。因此,下次遇到B时,首先在Map中搜索它,如果它有相应的值,则不需要再次检查。但它可能会使用更多的内存,所以关心它。希望它可以帮到你。

答案 6 :(得分:1)

使用VP tree可以解决此问题。这些允许查询数据 距离是服从三角不等式的度量。

VP树相对于k-D树的一大优势是它们可以盲目地使用 适用于世界任何地方的地理数据,无需担心 将其投射到合适的2D空间。另外还有一个真正的测地线 距离可以使用(无需担心之间的差异 投影中的测地距离和距离)。

这是我的测试:随机和均匀地产生500万点 世界。将它们放入VP树中。

循环遍历所有点,查询VP树以查找任何邻居a 距离(0km,10km)距离(0km不包括在此设置中以避免 找到查询点。)计算没有这样的点数 邻居(在我的情况下是229573)。

设置VP树的成本= 5000000 * 20距离计算。

查询成本= 5000000 * 23距离计算。

设置和查询的时间是5分7秒。

我正在使用带有GeographicLib的C ++来计算距离,但是 该算法当然可以用任何语言实现,这里是 python version of GeographicLib

ADDENDUM :实现此方法的C ++代码为here