Python地理空间坐标格式转换

时间:2017-01-24 05:57:02

标签: python-3.x gis

我有一个包含6列坐标对的数据框:Degrees | Minutes | Seconds(纬度和经度)。这称为NAD83格式。我想将它们转换为只有2列十进制格式的新数据帧,称为NAD27。

我通常使用的库,geopy几乎支持所有格式,因此实际上没有专用的转换功能。我在这里查看文档以确定: https://geopy.readthedocs.io/en/1.10.0/

python有没有其他方法可以转换为NAD27?

感谢您阅读

2 个答案:

答案 0 :(得分:3)

我们假设您的DataFrame df包含lonDlonMlonSlatDlatMlatS列。 然后,以下内容应该可以在内部使用geopandasshapelypyproj

import geopandas as gpd
import numpy as np
from shapely.geometry import Point

def dms_to_dec(d, m, s):
    sign = np.sign(d)
    return d + sign * m / 60 + sign * s / 3600

points = df.apply(lambda row: Point(dms_to_dec(*row[['lonD', 'lonM', 'lonS']]), 
                                    dms_to_dec(*row[['latD', 'latM', 'latS']])),
                  axis=1)
gdf_nad83 = gpd.GeoDataFrame(df, geometry=points, crs={'init': 'EPSG:4269'})
gdf_nad27 = gdf_nad83.to_crs({'init': 'EPSG:4267'})

答案 1 :(得分:1)

因为我也遇到了这个问题,并且发现df.apply()的方法太慢,所以我转而使用MultiPoint()对象并使用矢量化操作,然后将单个对象变成了Point() s与list()

此外,我们需要考虑DMS列可能仅在D列上包含-符号这一事实。如果是这样,并且您很幸运,DataFrame是使用numpy浮点数创建的,那么"-0.0"可能已存储为numpy.NZERO(负零),在这种情况下,我们仍然可以使用{{3 }}。如果不是这样,则该符号可能会丢失,并且指向赤道以南或第零子午线以西的点将显示为正北或向东。

只需明确一点:D,M,S坐标符号就是这样,这是记录纬度和经度坐标的另一种方式,其中D,M和S代表 degrees ((弧)分钟和(弧形)。十进制是另一个,它将度值与弧分和弧秒合并为一个数字;弧分是度的1/60,弧秒是度的1/3600,因此您可以做一些数学运算以将值加起来(保留度的符号)。 GeoPy希望使用十进制值,因此您需要将弧秒和弧分折成度值。

另一方面,NAD83和NAD27不是numpy.signbit(),并且此类系统与符号无关。它们只是指定使用什么坐标系以及将坐标系锚定到哪个参考点的标准化方法。

也就是说,大熊猫 可用于在不同的大地基准之间进行转换。该项目接受geodetic datums or geodetic systems来定义在解释点(大地基准是其组成部分)时要使用的坐标系;使用CRS strings之类的坐标系数据库来找到NAD83和NAD27的EPSG代码,分别得到https://spatialreference.org/EPSG:4269。请注意,您不必在此处创建数据框,只要您想要的只是 conversion GeoSeries就足够了。

因此,假设您具有度,分和秒,则需要将这些值转换为十进制坐标,以输入到地理熊猫。而且您想快速高效地执行此操作。您可以通过使用矢量化计算来做到这一点(其中numpy使用非常快速的算术运算直接对数据的机器表示而不是Python表示法将计算应用于所有行)。

我在这里遵循相同的约定,输入的是Pandas DataFrame df,其中包含列lonDlonMlonSlatDlatMlatS。使用geopandasnumpyshapely

import geopandas as gpd
import numpy as np
from shapely.geometry import asMultiPoint

def vec_dms_to_dec(d, m, s):
    """convert d, m, s coordinates to decimals

    Can be used as a vectorised operation on whole numpy arrays,
    each array must have the same shape.

    Handles signs only present on the D column, transparently.
    Note that for -0d Mm Ss inputs, the sign might be have been lost!
    However, if it was preserved as np.NZERO, this function will
    recover it with np.signbit().

    """
    assert d.shape == m.shape == s.shape
    # account for signs only present on d
    if (m >= 0).all() and (s >= 0).all():
        # all s and m values are without signs
        # so only d carries this info. Use the sign *bit* so negative
        # and positive zero are distinguished correctly.
        sign = np.where(np.signbit(d), np.ones_like(d) * -1.0, np.ones_like(d))
    else:
        sign = np.ones_like(d)
    return d + sign * m / 60 + sign * s / 3600

# Generate the column names, grouped by component
comps = ([f"{c}{a}" for c in ("lon", "lat")] for a in 'DMS')

# Create a single MultiPoint object from the vectorised conversions of the
# longitude and latitude columns
mpoint = asMultiPoint(
    vec_dms_to_dec(*(df[c].values for c in cols))
)

# Create a GeoSeries object from the MultiPoints object. Using `list()`
# produces `Point()` objects efficiently, faster than GeoSeries would
# otherwise.
# Interpret the points as using NAD83 == EPSG:4269
coords_nad83 = gpd.GeoSeries(list(mpoint), crs={'init': 'EPSG:4269'})

# Convert the series to NAD27 == EPSG:4267
coords_nad4267 = coords_nad83.to_crs(epsg=4267)

然后您可以自由地将它们再次转换为D,M,S表示法的值:

from shapely.geometry import MultiPoint

def geoseries_to_dms(s, all_signed=True):
    fractions, decimals = np.modf(np.array(MultiPoint(s.to_list())))
    if not all_signed:
        # only the d values signed. Looses information
        # for input values in the open range (-1.0, 0.0)
        fractions = np.abs(fractions)
    fractions, minutes = np.modf(fractions * 60)
    seconds = fractions * 60
    return pd.DataFrame(
        data=np.stack(
            (decimals, minutes, seconds), axis=2
        ).reshape(-1, 6),
        columns=loncols + latcols
    )

上面使用EPSG:4267从分数中分离出小数部分,然后将分数的绝对值再次划分为弧分和弧秒。

如果您仍想使用GeoDataFrame,请按照转换后的GeoSeries创建一个对象,或者仅使用创建MultiPoints()的方法从GeoSeries对象创建一个对象使用MultiPoints()GeoDataFrame(..., geometry=list(points), ...)对象中获取。

基准化,或者为什么要向量化

关于矢量化:上面的代码将度,分和秒列中的每个列作为三个单独的numpy数组,并使用这3个数组创建一个十进制度值的单个数组,跨所有行一步 em>。不需要单独调用纬度或经度值,因为numpy将dms作为数组处理,并且不关心它们是否只是数组一维或15。

这意味着执行速度大大提高。为了对此进行基准测试,让我们创建一个具有任意数量的dms坐标的新数据框;我发现仅生成十进制值并将其转换为dms值更容易:

import numpy as np
import pandas as pd
from shapely.geometry import Point, asMultiPoint

def random_world_coords(n):
    coords = np.random.random((2, n))
    coords[0] = coords[0] * 180 - 90  # lat between -90, 90
    coords[1] = coords[1] * 360 - 180 # lon between -180, 180
    # convert to d, m, s
    fractions, decimals = np.modf(coords)
    fractions, minutes = np.modf(fractions * 60)
    seconds = fractions * 60
    return pd.DataFrame(
        data=np.stack((decimals, minutes, seconds), axis=2).reshape(-1, 6),
        columns=["lonD", "lonM", "lonS", "latD", "latM", "latS"]
    )

并定义将这些值转换为适合GeoSeries()使用的小数点的方法作为函数。我删除了符号处理,因为所有dms列上的随机数据都包含符号,这也使得对标量和数组操作使用相同的转换函数变得很简单:

def dms_to_dec(d, m, s):
    """convert d, m, s coordinates to decimals"""
    return d + m / 60 + s / 3600

def martinvalgur_apply(df):
    return df.apply(
        lambda row: Point(
            dms_to_dec(*row[['lonD', 'lonM', 'lonS']]), 
            dms_to_dec(*row[['latD', 'latM', 'latS']])
        ),
        axis=1
    )

def martijnpieters_vectorised(df):
    comps = ([f"{c}{a}" for c in ("lon", "lat")] for a in 'DMS')
    return list(asMultiPoint(
        dms_to_dec(*(df[c].values for c in comps))
    ))

在这一点上,您可以使用IPython的%timeit或其他基准测试库来测试运行速度:

df100 = random_world_coords(100)
%timeit martinvalgur_apply(df100)
%timeit martijnpieters_vectorised(df100)
# 433 ms ± 15.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
# 96.2 ms ± 7.5 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

这是100个项目,向量化速度快了4.5倍。

如果将计数增加到1000,则数字的差异会变得更加明显:

df1000 = random_world_coords(1000)
%timeit martinvalgur_apply(df1000)
%timeit martijnpieters_vectorised(df1000)
# 4.31 s ± 111 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
# 35.7 ms ± 909 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

因此,在1000行中,矢量化仍仅需几毫秒,并且花费的时间更少 ,因为我们现在正在实现用于较大数据集的优化,但是使用了在这些数据集上运行df.apply()所花费的时间1000行已膨胀到4秒以上。

(注意:对于使用DataFrame.copy()创建的每个测试,我还使用输入的深拷贝运行测试,以确保我没有从已经处理过的数据中获益,但是时间仍然减少了, (对于100-> 1000行的情况)。

非矢量化版本所花费的时间与行数成正比,因此10k行的数目是可以预测的:

df10k = random_world_coords(10_000)
%timeit martinvalgur_apply(df10k)
%timeit martijnpieters_vectorised(df10k)
# 44.1 s ± 1.1 s per loop (mean ± std. dev. of 7 runs, 1 loop each)
# 331 ms ± 14.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

预计df.apply()版本需要44秒钟,但是我必须等待整整5分钟才能得到结果,因为IPython仍然运行了7次测试。

矢量化方法的时钟周期仅为331ms,因此我们可以在100万行的行中测试 just 版本:

df1m = random_world_coords(1_000_000)
%timeit martijnpieters_vectorised(df1m)
# 3.18 s ± 114 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

因此矢量化方法也线性缩放,但它的起点低得多。这段时间的大部分时间都用于从Point()对象创建MultiPoint()对象的列表,geopandas项目可以在np.modf()上得到改进。