提取访问多个(纬度,经度)的ID的最大距离

3
我有一个格式如下的表格:
用户 纬度 经度
u1 x1 y1
u1 x2 y2
u1 x3 y3
u2 x4 y4
u2 x5 y5
u2 x6 y6
u3 x7 y7
u3 x8 y8
我想要的是这样一张表格,对于每个用户,列出他们去过的两个地点间的最远距离:
用户 最大距离(公里)
u1 15.2
u2 23.7
u3 8.3
朴素的做法是循环每个用户,为每个用户创建一个距离矩阵并提取最大距离。但对于大量的用户这种方式不可扩展。
有没有更高效、更优雅的方法呢?

1
你可以使用旋转卡尺算法。也许有一个Python实现的版本。https://en.wikipedia.org/wiki/Rotating_calipers - Andrew
1
我的答案在这里,提供了三种测量两个点(由地理坐标表示)之间距离的选项,可能会有所帮助。链接 - Gonçalo Peres
对于您最初的效率缩放问题,如果将二维坐标转换为一维,那么最大值减去最小值是否可以给出答案? - S2L
@S2L,你如何将二维坐标转换为一维坐标? - mlx
5个回答

2

摘要

实现了一个快速算法,可在线性时间内运行

  • 美国城市数据集(30,409条记录):0.103秒
  • 动物追踪数据集(89,867条记录):0.325秒
  • 10多年的Windows桌面计时(i7 920 CPU @ 2.67GHz)

方法

具有线性复杂度,即O(N)

  • N是所有用户的纬度/经度总数(即跨越所有用户的数量)

执行以下步骤:

  1. 按用户分组纬度/经度数据
  2. 对每个用户重复步骤3-7
  3. 使用球形地球近似将纬度/经度点映射到x、y、z坐标
  4. 如下找到两个最远的点:
    • 将P1初始化为点的质心
    • 重复以下3次(通常一次就足够了,但多次处理角落情况):
      • 设置P0 = P1
      • 将P1设置为离P0最远的点
    • P0和P1是x、y、z中最远的两个点
  5. 使用P0和P1的索引从原始纬度/经度数据中查找纬度/经度
  6. 使用Haversine计算P0和P1之间的距离
  7. 用当前用户的距离更新结果
  8. 将所有用户的结果作为数据框返回

代码

import numpy as np

def lat_lon_to_xyz(lat, lon):
    '''
        Convert latitude/longitude to x, y, z in Earth centered coordinates (assuming spherical earth)
        
        lat, lon are in degrees radian
        
        Source: https://dev59.com/LnM_5IYBdhLWcg3w4nfb
    '''
    lat_radians = np.deg2rad(lat)
    lon_radians = np.deg2rad(lon)
    
    R = 1  # use unit sphere rather than 6371 radius of earth in km 
    x = R * np.cos(lat_radians) * np.cos(lon_radians)
    y = R * np.cos(lat_radians) * np.sin(lon_radians)
    z = R *np.sin(lat_radians)
    
    return np.array([x, y, z])
    
def furthest_points_spadsman(points):
    '''
        Based upon the following technique which scales linearly with the number of points
        
        - Initialize P1 to the center of mass of the points
        - Repeat the following 3 times (once is normally enough but multiple times handles corner cases):
          - Set P0 = P1
          - Set P1 = the point in points with maximum distance from P0
          - P0 and P1 are the furthest two points in x, y, z
        
        Technique from following reference.
        Reference: https://dev59.com/k2Qn5IYBdhLWcg3wZGZ4/
    '''
    # Initialize to mean
    p_1 = np.mean(points, axis = 0)
    
    for _ in range(3): # Iterating mitigates corner cases
        p_0 = p_1
        # Point in points furthest distance from p_0
        # note: can use squared distance since monotonical
        p_1 = points[np.argmax(np.sum(np.square(points - p_0), axis = -1))]
    
    return p_0, p_1

def haversine(point1, point2):
    '''
        Data in point1 and point2 are latitude/longitude pairs, 
        with first number is the latitude (north-south), 
        and the second number is the longitude (east-west)
        
        Source: https://medium.com/@petehouston/calculate-distance-of-two-locations-on-earth-using-python-1501b1944d97
    '''
    R = 6371  # Earth radius in km
    
    point1 = np.deg2rad(point1)
    point2 = np.deg2rad(point2)
    
    delta = point2 - point1
    
    a = (np.sin(delta[0] / 2) ** 2 + 
         np.cos(point1[0]) * np.cos(point2[0]) * np.sin(delta[1] / 2) ** 2)
    
    return 2 * R * np.arcsin(np.sqrt(a))
    
def process(df, user = 'user', lat_field ='lat', lon_field = 'lon'):
    '''
        Generates the Dataframe containing the maximum distance by user of a set of points
        
        The process works as following steps.
        1.  Group latitude/longitude data by user
        2.  Repeat steps 3-7 for each user
        3.  Map latitudes/longitudes points to x, y, z coordinates using spherical earth approximation)
        4.  Find two furthest points as follows:
            i. calculate the center of mass M of the points
            ii. find the point P0 that has the maximum distance to M
            iii. find the point P1 that has the maximum distance to P0
            iv. P0 and P1 are the furthest two points in x, y, z
        5. Use indexes of P0 & P1 to lookup latitude/longitude from original lat/log data
        6. Calcualte distance between P0 & P1 using Haversine
        7. Update results
        8. Return results as a dataframe
        
         Process based upon following references:
         a. https://dev59.com/k2Qn5IYBdhLWcg3wZGZ4#16870359
         b. https://medium.com/@petehouston/calculate-distance-of-two-locations-on-earth-using-python-1501b1944d97
    
    '''
    results = []                              # holds list of tuples of (user, distance)
    for user_, g in df.groupby(user):            # Step 1--Group latitude/longitude data by user
        # Step 2--Repeat steps 2-4 for each user
        points_lat_lon = g[[lat_field, lon_field]].to_numpy()

        # Step 3--map latitudes/longitudes points to x, y, z coordinates
        points_xyz = lat_lon_to_xyz(points_lat_lon[:, 0], points_lat_lon[:, 1]).transpose()

        # Step 4--Find two furthest points
        # Find two furthest points in xyz (using spherical earth aproximation)
        p_0, p_1 = furthest_points_spadsman(points_xyz)
        
        # Step 5--Use indexes of P0 & P1 to lookup latitude/longitude from original lat/log data
        # Index of p_0 and p_1 in points_xyz (so we also corresponds to the index in points_lat_lon)
        index_0 = np.where(np.prod(points_xyz == p_0, axis = -1))[0][0]
        index_1 = np.where(np.prod(points_xyz == p_1, axis = -1))[0][0]

        lat_lon_0 = points_lat_lon[index_0, :]
        lat_lon_1 = points_lat_lon[index_1, :]
     
        # Step 6--Calcualte distance between P0 & P1 using Haversine
        distance = haversine(lat_lon_0, lat_lon_1)
        
        # Step 7--update results
        results.append((user_, distance))
    
    # Step 8--Return results as a dataframe
    return pd.DataFrame(results, columns = [user, 'Max_Distance_km'])

测试

测试1

描述

计算美国城市之间的最大距离

  • 使用州ID作为用户
  • 总共有30,409条记录(每个城市和州有多条记录)
  • 每个记录包括州ID、纬度和经度
  • 处理30,409条记录的时间:在10年以上的Windows桌面电脑上为0.104秒(i7 920 CPU @ 2.67GHz)

数据集

  • 从此网站下载:simplemaps
  • 每个州包含许多城市
  • 使用州ID作为用户(即找到每个州之间的最大距离)

测试代码

from time import time
import pandas as pd

# CSV file downloadable from https://simplemaps.com/data/us-cities
# Datafile with 30, 409 records
cities = pd.read_csv('simplemaps_uscities_basicv1.75/uscities.csv')

t0 = time()
result = process(cities, user = 'state_id', lat_field = 'lat', lon_field = 'lng')
print(f'Processing time: {time()-t0:.3f} seconds')
print(f'Results:\n{result}')

输出

Processing time: 0.104 seconds
Results:
   state_id  Max_Distance_km
0        AK      3586.855864
1        AL       569.292071
2        AR       492.544129
3        AZ       712.434590
4        CA      1321.284443
5        CO       697.572158
6        CT       182.286421
7        DC         0.000000
8        DE       156.778146
9        FL       936.595405
10       GA       589.700716
11       HI       574.129490
12       IA       538.297210
13       ID       825.044994
14       IL       622.014829
15       IN       496.787181
16       KS       682.563079
17       KY       633.576282
18       LA       601.891459
19       MA       301.815349
20       MD       397.753918
21       ME       509.556000
22       MI       743.578849
23       MN       751.324104
24       MO       707.260076
25       MS       534.872877
26       MT       961.640222
27       NC       778.308918
28       ND       582.080515
29       NE       763.370612
30       NH       249.275265
31       NJ       259.273945
32       NM       747.581138
33       NV       807.834661
34       NY       641.785757
35       OH       471.708115
36       OK       826.431505
37       OR       649.340103
38       PA       508.693319
39       PR       205.710138
40       RI        81.539958
41       SC       435.894534
42       SD       688.135798
43       TN       751.286457
44       TX      1240.972424
45       UT       611.262766
46       VA       729.361836
47       VT       285.877877
48       WA       616.073484
49       WI       570.813035
50       WV       441.834382
51       WY       682.873519

测试2

描述

查找动物追踪数据中动物行程的最远距离。

  • 126个不同的动物标签(例如用户)
  • 89,867条数据记录
  • 处理时间为0.325秒

数据集

  • Movebank是由Max Planck动物行为研究所托管的动物追踪数据的在线数据库。
  • 使用了Kaggle上的Movebank数据集。
  • 数据来源

测试代码

from time import time
import pandas as pd

# Data downloaded from above kaggle link
df = pd.read_csv('migration_original.csv/migration_original.csv')

t0 = time()
result = process(df, user = 'individual-local-identifier', lat_field = 'location-lat', lon_field = 'location-long')
print(f'Processing time: {time()-t0:.3f} seconds')
print(f'Results:\n{result}')

输出

Processing time: 0.325 seconds
Results:
    individual-local-identifier  Max_Distance_km
0                        91732A      7073.629785
1                        91733A        65.788571
2                        91734A      3446.277830
3                        91735A       231.789762
4                        91737A      5484.820693
..                          ...              ...
121                      91920A      2535.920902
122                      91921A        26.698255
123                      91924A        14.518173
124                      91929A         0.806871
125                      91930A        10.427890

[126 rows x 2 columns]

参考文献

致谢

  • 感谢@MangoNrFiv的评论,帮助改进了实现和测试。

作为我之前评论的澄清,举个例子。为了更容易理解,只需考虑赤道周围(0°N)的位置:在0°E处有一组位置;在90°E处有一个位置;在90°W处有一个位置;在100°E处有一个位置。您的方法会找到100°E点和90°W点,而实际上应该是在90°E和90°W处的那些点。 - MangoNrFive
是的,这似乎是一个非常困难的问题,但将其转换为x、y、z坐标,然后直接计算距离而不使用haversine本身就是一个巨大的改进。因此,您仅凭该建议就获得了我的投票。 - MangoNrFive
@MangoNrFive -- 感谢您的反馈。我会更深入地研究您的示例。这种方法允许您使用x、y、z来找到极点,然后在它们上面使用Haversine算法来找到极点之间的距离。但是,我必须要在我的写作中提到参考文献所提出的想法。 - DarrylG
@MangoNrFive -- 根据 np.max(np.sum(np.square(points - pt))) 的建议 -- 这不会给我最大距离的值,而不是具有最大距离的 points 中的点吗? - DarrylG
让我们在聊天中继续这个讨论 - DarrylG
显示剩余6条评论

1
你是否可以使用笛卡尔距离代替大圆距离?在你所描述的范围内,它们应该非常相似。如果可以的话,请按照此文档第4页的说明将纬度/经度转换为ECEF(地心地固)笛卡尔坐标。然后,对于每个用户的ECEF位置向量集合,可以使用Megiddo的1983年最小外接球算法在O(n)时间内计算出两个最远点之间的距离。如果需要大圆距离,则可能可以在球面坐标系中应用Welzl算法,但这似乎是一个相当大的任务。
更严格地说,包围球的直径提供了两个最远点之间距离的上限,而球面上一组点中两个最远点之间的距离则提供了下限。如果只有两个点位于球面上,则这些点必须是对踵点,并且一定是最远的。否则,可以通过删除未远离球心足以成为最大分离点对(基于先前确定的下限)的任何点来缩小可能的点对搜索空间,但是必须使用另一种方法评估缩小后的空间。

1
请注意,包围球上的点不一定是距离最远的一对点的成员。假设封闭球由等边三角形的点定义,则我们可以在该球内放置两个点,它们之间的距离(几乎)为2半径。 - Willem Hendriks
编辑以解决上述提出的很好的观点。我相信现在完全正确,但我可能错了。感谢@WillemHendriks,如果您有进一步的意见,请再次评论。 :-) - Jeremy
确实!您的想法在实践中可能已经足够好,可以立即在缩小的空间上进行全面搜索。如果您将条件从球体放宽到最接近的N个点,那就更好了。创意十足! - Willem Hendriks

1
在这个答案中,您将找到两个潜在的选项:
1.选项1,使用我在这里的答案中创建的函数。在那个答案中,您将找到其他可以使用的方法。
2.选项2,使用不同的函数。
为了测试目的,即使我建议尽可能接近您将要使用的数据进行测试,我也将采用由@Qdr提出的示例
import pandas as pd
import numpy as np
import random as rn

data = [[rn.randint(1, 10), rn.randint(1, 10)] for x in range(9)]
users = ['user1', 'user2', 'user3'] * 3
rn.shuffle(users)

df1 = pd.DataFrame(data, columns=['x', 'y'], index=users)

选项1

为了测量两点之间的距离(由地理坐标表示),如我上面所提到的,可以使用我在这里分享的函数之一,其中我们将找到更好的解释。

该函数称为haversine,受Haversine公式的启发而来。

def haversine(lon1, lat1, lon2, lat2):
    """
    Calculate the great-circle distance (in km) between two points 
    using their longitude and latitude (in degrees).
    """
    # Radius of the Earth
    r = 6371.0

    # Convert degrees to radians 
    # First point
    lat1 = radians(lat1)
    lon1 = radians(lon1)

    # Second Point
    lat2 = radians(lat2)
    lon2 = radians(lon2)

    # Haversine formula 
    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 r * c

由于我们想要最大距离,因此让我们创建一个使用先前函数的函数。

def max_distance(lat1, lon1, lat2, lon2):
    # Calculate distance between two points
    distance = haversine(lon1, lat1, lon2, lat2)
    # Return max distance
    return np.max(distance)

最后,可以创建一个新的数据框 df2
[In]: df2 = df1.groupby(df1.index).apply(lambda x: pd.Series({'max_distance': max_distance(x['x'].iloc[0], x['y'].iloc[0], x['x'].iloc[1], x['y'].iloc[1])}))

[Out]:       max_distance
user1    866.714728
user2    867.428750
user3    247.358878

选项2

根据需求,以下函数也可以用于计算两点之间的最大距离,假设想要计算最大距离,则使用以下函数即可。

def max_distance(lat1, lon1, lat2, lon2):
    # Calculate distance between two points
    distance = np.sqrt((lat1 - lat2)**2 + (lon1 - lon2)**2)
    # Return max distance
    return np.max(distance)

为了创建一个新的数据框,按照用户进行分组(在这个例子中,使用数据框df1的索引),其中包含一个名为max_dist_km的列,该列将具有给定用户之间的最大距离(使用前面的函数),应执行以下操作。
df2 = df1.groupby(df1.index).apply(lambda x: pd.Series({'max_distance': max_distance(x['x'].iloc[0], x['y'].iloc[0], x['x'].iloc[1], x['y'].iloc[1])}))

谢谢!我有点困惑。在您定义的“max_distance”函数中,lat1(和其他变量)应该是数组还是标量?我的理解是它们是标量,但我不确定np.max在这里的作用。 - mlx
@mlx 你可能想要检查我刚分享的选项1。它使用了Haversine公式的实现。 - Gonçalo Peres

0

这种方法使用pandas groupby与sklearn空间函数相结合。速度相当快(大约与@DarrylG相同)。

我们定义了一个自定义的groupby函数,使用Convex Hull在组内提取边缘点,并使用Distance Metric Haversine计算最大距离。

这个想法是最大距离可以通过仅考虑凸包的边缘来进行尖锐的近似。由于滥用它用于纬度/经度对而导致其不足的边缘情况存在。

ConvexHull

import pandas as pd
import numpy as np

from sklearn.metrics import DistanceMetric
from scipy.spatial import ConvexHull

from math import radians

dist = DistanceMetric.get_metric('haversine')

def max_distance_within_group(df):
    
    EARTH_RADIUS = 6371.009
    
    group_gps = df[['location-lat','location-long']].values
    
    if len(group_gps) > 10:
        """
            If more than 10 point, lets create a convex-hull,
            and only use the edge points.
        """
        convex_hull_idx = ConvexHull(group_gps)
        group_gps = group_gps[convex_hull_idx.vertices]

    haversine_distances = dist.pairwise(np.radians(group_gps))
    haversine_distances *= EARTH_RADIUS

    return np.max(haversine_distances)

我使用了与@DarrylG相同的第二个测试用例,以便您可以进行比较。我们的速度非常相似,以至于我无法确定哪个更快。

migration = pd.read_csv('work/migration_original.csv')

应用

migration.groupby('individual-local-identifier').apply( max_distance_within_group )

它返回

individual-local-identifier
91732A    7073.639777
91733A      65.788664
91734A    3446.282699
91735A     231.790090
91737A    5484.828441
             ...     
91920A    2535.924485
91921A      26.698292
91924A      14.518194
91929A       0.806872
91930A      10.427905
Length: 126, dtype: float64

0

你可以在 scipy 中使用 distance_matrix

首先创建一个包含随机值和 3 个用户的数据框

import pandas as pd
from scipy.spatial import distance_matrix
import random as rn

    
data = [[rn.randint(1, 10), rn.randint(1, 10)] for x in range(9)]
users = ['user1', 'user2', 'user3'] * 3
rn.shuffle(users)

df = pd.DataFrame(data, columns=['x', 'y'], index=users)
df
x y
用户2 9 7
用户2 5 4
用户3 3 10
用户1 8 3
用户1 5 7
用户1 8 5
用户2 10 2
用户3 3 9
用户3 2 2

然后进行分组并应用距离矩阵

df.groupby(df.index).apply(lambda x: distance_matrix(x, x).max())

输出:

user1    5.000000
user2    5.385165
user3    8.062258
dtype: float64

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接