Python高效创建密度图

3
我希望能为提高代码运行速度提供一些帮助。
具体而言,我有一个包含纬度和经度点的方格列表“insideoceanlist”。然后有一个目录,其中包含特定日期闪电打击的“lat, long”坐标的数据文件。每天的想法是要知道每个点周围有多少闪电打击。目前只是两个for循环,因此对于方格上的每个点,您都要检查那天每次闪电打击距离它有多远。如果在40公里内,则在该点处添加1以制作密度图。
起始网格的整体形状类似于长方形,由宽度为0.11,长度为0.11的正方形组成。整个矩形约为50x30。最后我有一个形状文件,用于勾画澳大利亚的“预测区域”,如果网格中的任何点在此区域之外,则我们省略它。因此,所有剩余的点(“insideoceanlist”)都是澳大利亚的点。
方格网格上约有100,000个点,即使在低峰期也会有大约1,000次闪电击打,因此处理时间很长。有没有更有效的方法?非常感谢您的建议。
顺便说一下,我将“list2”更改为“list3”,因为我听说在Python中迭代列表比数组更快。
for i in range(len(list1)): #list1 is a list of data files containing lat,long coords for lightning strikes for each day
    dict_density = {}
    for k in insideoceanlist: #insideoceanlist is a grid of ~100000 lat,long points
        dict_density[k] = 0
    list2 = np.loadtxt(list1[i],delimiter = ",") #this open one of the files containing lat,long coords and puts it into an array
    list3 = map(list,list2) #converts the array into a list
    # the following part is what I wanted to improve
    for j in insideoceanlist:
        for l in list3:
            if great_circle(l,j).meters < 40000: #great_circle is a function which measures distance between points the two lat,long points
                dict_density[j] += 1
    #
    filename = 'example' +str(i) + '.txt'
        with open(filename, 'w') as f:
            for m in range(len(insideoceanlist)):
                f.write('%s\n' % (dict_density[insideoceanlist[m]])) #writes each point in the same order as the insideoceanlist
    f.close()

1
一个点的正方形网格可以用数学函数来描述,而不是存储在列表中。同样,一个点落入哪个正方形(相当于它最接近哪个正方形的中心点)可以直接计算,而不需要进行大量比较。 - Dan Getz
通过什么数学函数?我不太确定如何在不迭代每个正方形的情况下检查一个点落在哪个正方形中。每个正方形大约有10公里宽,所以如果我得到了这个,那么我只需要检查每个点是否在附近的正方形中吗?谢谢。 - tpup1
编写一个函数,将闪电击中的位置转换为包含它们的网格方块。这可能意味着将纬度/经度值四舍五入/截断到您正在使用的任何网格。现在估计在40,000米中有多少个0.11度(?)增量(在最小情况下)。对于每次打击,向{上,下,左,右}移动那么多增量,计算距离,并在条件成立时递增。 - aghast
将O(100M)转换为O(1000*x²),其中x是一个非常小的数字(40km中有多少个0.11度增量)。 - aghast
1
原来随着你向南/北移动,一度的大小也会有所不同。但是假设你的范围在澳大利亚附近,你可能处于-10..-40度的范围内。根据wikipedia的数据,在45度时一度约为80公里。因此,在最坏的情况下,40公里的范围将是半度,或者是5个0.11度的正方形。因此,您可以找到最接近的网格正方形,然后搜索121个正方形(上面5个,中心,下面5个),以查看它们是否在您的40公里圆圈内。这意味着对于1k次打击,您需要执行121k次操作,如果您聪明一些则可以少一些。 - aghast
显示剩余3条评论
3个回答

3
稍微解释一下@DanGetz的答案,这里有些代码使用strike数据作为驱动,而不是为每个strike点迭代整个grid。我假设您以澳大利亚中心点为中心,使用0.11度网格正方形,尽管一个度的大小随纬度变化! 一些简易计算加上对维基百科的快速参考告诉我,您的40公里距离是从南到北±4个网格范围,从东到西±5个网格范围。(在低纬度时它会降至4个正方形,但......)
关键在于,如上所述,要以直接的公式方式将打击位置(lat/lon)转换为网格正方形。找出网格一个角的位置,将该位置从打击位置中减去,然后除以网格大小-0.11度,截断,即可得到行/列索引。现在访问所有周围的正方形,直到距离增长太大,最多检查1 + (2 * 2 * 4 * 5) = 81个正方形的距离。增加范围内的正方形。
结果是我最多做了81次访问乘以1000个strike(或者你有多少),而不是访问100000个网格正方形乘以1000个strike。这是一个重大的性能提升。
请注意,您没有描述您的输入数据格式,所以我随机生成数字。您需要修复它。;-)
#!python3

"""
Per WikiPedia (https://en.wikipedia.org/wiki/Centre_points_of_Australia)

Median point
============

The median point was calculated as the midpoint between the extremes of
latitude and longitude of the continent.

    24 degrees 15 minutes south latitude, 133 degrees 25 minutes east
    longitude (24°15′S 133°25′E); position on SG53-01 Henbury 1:250 000
    and 5549 James 1:100 000 scale maps.

"""
MEDIAN_LAT = -(24.00 + 15.00/60.00)
MEDIAN_LON = (133 + 25.00/60.00)

"""
From the OP:

The starting grid has the overall shape of a rectangle, made up of
squares with width of 0.11 and length 0.11. The entire rectange is about
50x30. Lastly I have a shapefile which outlines the 'forecast zones' in
Australia, and if any point in the grid is outside this zone then we
omit it. So all the leftover points (insideoceanlist) are the ones in
Australia.
"""

DELTA_LAT = 0.11
DELTA_LON = 0.11

GRID_WIDTH = 50.0 # degrees
GRID_HEIGHT = 30.0 # degrees

GRID_ROWS = int(GRID_HEIGHT / DELTA_LAT) + 1
GRID_COLS = int(GRID_WIDTH / DELTA_LON) + 1

LAT_SIGN = 1.0 if MEDIAN_LAT >= 0 else -1.0
LON_SIGN = 1.0 if MEDIAN_LON >= 0 else -1.0

GRID_LOW_LAT = MEDIAN_LAT - (LAT_SIGN * GRID_HEIGHT / 2.0)
GRID_HIGH_LAT = MEDIAN_LAT + (LAT_SIGN * GRID_HEIGHT / 2.0)
GRID_MIN_LAT = min(GRID_LOW_LAT, GRID_HIGH_LAT)
GRID_MAX_LAT = max(GRID_LOW_LAT, GRID_HIGH_LAT)

GRID_LOW_LON = MEDIAN_LON - (LON_SIGN * GRID_WIDTH / 2.0)
GRID_HIGH_LON = MEDIAN_LON + (LON_SIGN * GRID_WIDTH / 2.0)
GRID_MIN_LON = min(GRID_LOW_LON, GRID_HIGH_LON)
GRID_MAX_LON = max(GRID_LOW_LON, GRID_HIGH_LON)

GRID_PROXIMITY_KM = 40.0

"""https://en.wikipedia.org/wiki/Longitude#Length_of_a_degree_of_longitude"""
_Degree_sizes_km = (
    (0,  110.574, 111.320),
    (15, 110.649, 107.551),
    (30, 110.852, 96.486),
    (45, 111.132, 78.847),
    (60, 111.412, 55.800),
    (75, 111.618, 28.902),
    (90, 111.694, 0.000),
)

# For the Australia situation, +/- 15 degrees means that our worst
# case scenario is about 40 degrees south. At that point, a single
# degree of longitude is smallest, with a size about 80 km. That
# in turn means a 40 km distance window will span half a degree or so.
# Since grid squares a 0.11 degree across, we have to check +/- 5
# cols.

GRID_SEARCH_COLS = 5

# Latitude degrees are nice and constant-like at about 110km. That means
# a .11 degree grid square is 12km or so, making our search range +/- 4
# rows.

GRID_SEARCH_ROWS = 4

def make_grid(rows, cols):
    return [[0 for col in range(cols)] for row in range(rows)]

Grid = make_grid(GRID_ROWS, GRID_COLS)

def _col_to_lon(col):
    return GRID_LOW_LON + (LON_SIGN * DELTA_LON * col)

Col_to_lon = [_col_to_lon(c) for c in range(GRID_COLS)]

def _row_to_lat(row):
    return GRID_LOW_LAT + (LAT_SIGN * DELTA_LAT * row)

Row_to_lat = [_row_to_lat(r) for r in range(GRID_ROWS)]

def pos_to_grid(pos):
    lat, lon = pos

    if lat < GRID_MIN_LAT or lat >= GRID_MAX_LAT:
        print("Lat limits:", GRID_MIN_LAT, GRID_MAX_LAT)
        print("Position {} is outside grid.".format(pos))
        return None

    if lon < GRID_MIN_LON or lon >= GRID_MAX_LON:
        print("Lon limits:", GRID_MIN_LON, GRID_MAX_LON)
        print("Position {} is outside grid.".format(pos))
        return None

    row = int((lat - GRID_LOW_LAT) / DELTA_LAT)
    col = int((lon - GRID_LOW_LON) / DELTA_LON)

    return (row, col)


def visit_nearby_grid_points(pos, dist_km):
    row, col = pos_to_grid(pos)

    # +0, +0 is not symmetric - don't increment twice
    Grid[row][col] += 1

    for dr in range(1, GRID_SEARCH_ROWS):
        for dc in range(1, GRID_SEARCH_COLS):
            misses = 0
            gridpos = Row_to_lat[row+dr], Col_to_lon[col+dc]
            if great_circle(pos, gridpos).meters <= dist_km:
                Grid[row+dr][col+dc] += 1
            else:
                misses += 1
            gridpos = Row_to_lat[row+dr], Col_to_lon[col-dc]
            if great_circle(pos, gridpos).meters <= dist_km:
                Grid[row+dr][col-dc] += 1
            else:
                misses += 1
            gridpos = Row_to_lat[row-dr], Col_to_lon[col+dc]
            if great_circle(pos, gridpos).meters <= dist_km:
                Grid[row-dr][col+dc] += 1
            else:
                misses += 1
            gridpos = Row_to_lat[row-dr], Col_to_lon[col-dc]
            if great_circle(pos, gridpos).meters <= dist_km:
                Grid[row-dr][col-dc] += 1
            else:
                misses += 1
            if misses == 4:
                break

def get_pos_from_line(line):
    """
    FIXME: Don't know the format of your data, just random numbers
    """
    import random
    return (random.uniform(GRID_LOW_LAT, GRID_HIGH_LAT),
            random.uniform(GRID_LOW_LON, GRID_HIGH_LON))

with open("strikes.data", "r") as strikes:
    for line in strikes:
        pos = get_pos_from_line(line)
        visit_nearby_grid_points(pos, GRID_PROXIMITY_KM)

1
如果你知道生成网格点的公式,那么可以通过反转该公式快速找到给定点最近的网格点。
下面是一个激励性的例子,但对于你的目的来说并不完全正确,因为地球是一个球体,而不是平面或圆柱体。如果不能轻松地反转网格点公式以找到最近的网格点,则可以尝试以下方法:
  • 创建第二个网格(我们称之为G2),它是一个简单的公式,如下所示,具有足够大的方块,以便您可以确信任何一个方块中最接近的网格点要么在同一方块中,要么在8个相邻的方块之一。
  • 创建一个dict,其中存储原始网格(G1)中哪些点位于G2网格的哪个方块中
  • 获取您要分类的点p,并找到它将进入的G2方块
  • p与此G2方块中的所有G1点以及该方块的所有直接相邻点进行比较
  • 选择其中距离p最近的G1

带有完美平面网格的激励示例

如果您在平面表面上有一个完美的正方形网格,它没有旋转,边长为d,则它们的点可以用一个简单的数学公式来定义。它们的纬度值将全部为

lat0 + d * i

对于某个整数值 i,其中 lat0 是最低编号的纬度,它们的经度值将具有相同的形式:

long0 + d * j

对于某个整数j,为了找到给定(lat, long)对应的最近网格点,您可以分别找到它的纬度和经度。 您的网格上最接近的纬度数将是

i = round((lat - lat0) / d)

同样地,对于经度,可以使用j = round((long - long0) / d)

因此,你可以将其代入上述公式中,得到

grid_point = (lat0 + d * round((lat - lat0) / d),
              long0 + d * round((long - long0) / d)

只需在该网格点的dict中递增计数即可。这将使您的代码比以前快得多,因为您直接通过一些计算找到了距离最近的网格点,而不是检查成千上万个网格点。

您可以通过使用ij数字作为多维数组的索引,而不是在dict中使用grid_point,来进一步提高速度。


随着纬度的增加,经度之间的距离越来越接近。 - aghast
@AustinHastings 哎呀,我就知道这需要更复杂的方法。不过还是有类似的解决方案的。 - Dan Getz
1
我不习惯处理经度,所以我想我会加上一个警告并希望这可以帮助到大家,直到有人给出更好的答案。为什么我们不能生活在一个环面上? - Dan Getz
根据我们所知,我认为最坏情况是+/-5个网格方块。因此,代码可以从网格点开始,向外工作,直到距离度量失败。 - aghast
这真的帮了大忙,谢谢。如果你假设闪电击中了网格的顶部(在这种情况下是-7.25度纬度),你可以检查有21个点始终在40公里范围内。然后,如果你假设它在底部,最多还有44个点可能在40公里范围内。稍后我可能会链接我所做的内容。 - tpup1
@tpup1 看起来我误解了你的意图,而且不止一种方式,但无论如何,我很高兴这对你有所帮助! - Dan Getz

0

你尝试过使用Numpy进行索引吗?你可以使用多维数组,由于Numpy数组本质上是Python对C数组的包装器,所以索引应该更快。

如果你需要进一步提高速度,请看看Cython,这是一个Python到优化C转换器。它特别适用于多维索引,并且应该能够将这种类型的代码加速约一个数量级。它会为你的代码添加一个额外的依赖项,但安装很快,实现也不太困难。

(Benchmarks), (Tutorial using Numpy with Cython)

另外,作为一个快速提示,使用

for listI in list1:
    ...
    list2 = np.loadtxt(listI, delimiter=',')
 # or if that doesn't work, at least use xrange() rather than range()

基本上,只有在需要 range() 函数生成的列表时才应该使用 range()。在您的情况下,它不会做太多事情,因为它是最外层的循环。


谢谢Justin,我一定会看看Cython。我不确定你所说的索引是什么意思。我已经尝试使用数组'list2'进行迭代,但速度有点慢。关于range()的好建议,谢谢。 - tpup1

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