第23章:地理编码(Geocoding)
地理编码是将人类可读的地址信息转换为地理坐标的过程,是地理信息系统中连接文本地址与空间位置的关键桥梁。GeoPandas 通过集成 geopy 库,提供了简洁高效的地理编码接口,使得在 Python 数据分析工作流中轻松实现地址与坐标之间的相互转换。本章将全面介绍 GeoPandas 中的地理编码功能,包括正向编码、逆向编码、批量处理、性能优化以及实际应用案例。
23.1 地理编码概述
23.1.1 什么是地理编码
地理编码(Geocoding)是地理信息处理中的一项基础技术,主要包含两个方向的转换:
正向地理编码(Forward Geocoding):将文本地址转换为地理坐标(经纬度)。例如,将”北京市海淀区中关村南大街1号”转换为经度 116.326、纬度 39.965。
逆向地理编码(Reverse Geocoding):将地理坐标(经纬度)转换为人类可读的地址信息。例如,将坐标 (116.397, 39.908) 转换为”北京市东城区天安门广场”。
# 地理编码的基本概念示意
# 正向地理编码:地址 -> 坐标
# "北京市朝阳区望京街道" -> (116.480, 40.002)
# 逆向地理编码:坐标 -> 地址
# (121.474, 31.230) -> "上海市黄浦区南京东路"
地理编码的核心原理是通过查询地理数据库,将地址字符串进行解析、匹配,最终返回对应的空间位置信息。现代地理编码服务通常基于以下技术:
- 地址解析:将地址字符串拆分为省、市、区、街道、门牌号等结构化组件
- 模糊匹配:处理地址中的拼写错误、缩写、别名等不规范情况
- 空间索引:利用空间索引技术加速坐标查找过程
23.1.2 地理编码的应用场景
地理编码在众多领域有着广泛的应用:
| 应用领域 | 具体场景 | 说明 |
|---|---|---|
| 物流配送 | 订单地址转坐标 | 计算配送路线和距离 |
| 商业分析 | 门店选址分析 | 将客户地址转为坐标进行空间分析 |
| 城市规划 | 设施分布分析 | 将地址数据转换为可视化地图 |
| 房地产 | 房源位置标注 | 在地图上展示房源分布 |
| 公共卫生 | 疫情数据地理化 | 将病例地址转为坐标进行空间聚类分析 |
| 应急管理 | 事故定位 | 快速将报警地址转为精确坐标 |
23.2 GeoPandas geocoding 接口
23.2.1 依赖安装
GeoPandas 的地理编码功能依赖于 geopy 库。geopy 是一个 Python 地理编码客户端库,支持多种地理编码服务提供商。
# 安装 geopy
# pip install geopy
# 验证安装
import geopy
print(f"geopy 版本: {geopy.__version__}")
输出:
geopy 版本: 2.4.1
如果需要完整的地理编码环境,建议同时安装以下依赖:
# 完整安装命令
# pip install geopandas geopy requests
# 验证所有依赖
import geopandas as gpd
import geopy
from geopy.geocoders import Nominatim
print(f"GeoPandas 版本: {gpd.__version__}")
print(f"geopy 版本: {geopy.__version__}")
print("所有依赖安装成功!")
输出:
GeoPandas 版本: 1.0.1
geopy 版本: 2.4.1
所有依赖安装成功!
23.2.2 接口架构
GeoPandas 提供了两个核心地理编码函数,位于 geopandas.tools 模块中:
import geopandas as gpd
# GeoPandas 地理编码接口概览
# 1. 正向地理编码
# gpd.tools.geocode(strings, provider, **kwargs)
# 2. 逆向地理编码
# gpd.tools.reverse_geocode(points, provider, **kwargs)
接口架构的核心设计如下:
| 组件 | 说明 |
|---|---|
gpd.tools.geocode() |
正向地理编码,地址转坐标 |
gpd.tools.reverse_geocode() |
逆向地理编码,坐标转地址 |
geopy.geocoders |
底层服务提供商接口 |
provider 参数 |
指定使用的编码服务提供商 |
GeoPandas 地理编码接口的工作流程:
# 接口调用链路示意
# 用户调用 gpd.tools.geocode()
# -> 创建 geopy geocoder 实例
# -> 发送 HTTP 请求到服务提供商 API
# -> 解析返回的 JSON 结果
# -> 构建 GeoDataFrame 返回
23.3 gpd.tools.geocode() 用法
23.3.1 基本用法
gpd.tools.geocode() 是 GeoPandas 中执行正向地理编码的核心函数:
import geopandas as gpd
# 基本地理编码示例
addresses = [
"Beijing, China",
"Shanghai, China",
"Guangzhou, China"
]
# 使用 Nominatim 提供商进行地理编码
result = gpd.tools.geocode(
addresses,
provider="nominatim",
user_agent="geopandas_tutorial"
)
print(type(result))
print(result)
输出:
<class 'geopandas.geodataframe.GeoDataFrame'>
geometry address
0 POINT (116.39723 39.90750) Beijing, China
1 POINT (121.46941 31.23170) Shanghai, China
2 POINT (113.26436 23.12908) Guangzhou, China
返回结果是一个标准的 GeoDataFrame,包含 geometry 列(Point 类型)和 address 列(标准化后的地址)。
23.3.2 参数详解
gpd.tools.geocode() 函数的完整参数说明:
# 函数签名
# gpd.tools.geocode(
# strings, # 地址列表或 Series
# provider=None, # 服务提供商名称或 geopy geocoder 实例
# user_agent=None, # 用户代理标识
# timeout=None, # 请求超时时间(秒)
# **kwargs # 传递给 geopy geocoder 的额外参数
# )
# 参数详解示例
addresses = ["Nanjing, China", "Hangzhou, China"]
# 使用字符串指定提供商
result1 = gpd.tools.geocode(
strings=addresses, # 必需:地址列表
provider="nominatim", # 提供商名称
user_agent="my_application", # Nominatim 必需的用户代理
timeout=10 # 超时时间10秒
)
print("方式一(字符串提供商):")
print(result1[["address"]].to_string())
输出:
方式一(字符串提供商):
address
0 Nanjing, Jiangsu, China
1 Hangzhou, Zhejiang, China
# 使用 geopy geocoder 实例作为提供商
from geopy.geocoders import Nominatim
# 创建自定义 geocoder 实例
geocoder = Nominatim(
user_agent="geopandas_tutorial",
timeout=15,
domain="nominatim.openstreetmap.org"
)
result2 = gpd.tools.geocode(
strings=addresses,
provider=geocoder # 直接传入 geocoder 实例
)
print("方式二(geocoder 实例):")
print(result2[["address"]].to_string())
输出:
方式二(geocoder 实例):
address
0 Nanjing, Jiangsu, China
1 Hangzhou, Zhejiang, China
23.3.3 返回结果结构
地理编码返回的 GeoDataFrame 具有以下结构:
import geopandas as gpd
addresses = ["Chengdu, China", "Wuhan, China", "Xi'an, China"]
result = gpd.tools.geocode(
addresses,
provider="nominatim",
user_agent="geopandas_tutorial"
)
# 查看结果结构
print("列名:", result.columns.tolist())
print("数据类型:")
print(result.dtypes)
print(f"\nCRS: {result.crs}")
print(f"行数: {len(result)}")
# 提取坐标信息
result["longitude"] = result.geometry.x
result["latitude"] = result.geometry.y
print("\n坐标信息:")
print(result[["address", "longitude", "latitude"]].to_string())
输出:
列名: ['geometry', 'address']
数据类型:
geometry geometry
address object
dtype: object
CRS: EPSG:4326
行数: 3
坐标信息:
address longitude latitude
0 Chengdu, Sichuan, China 104.06574 30.57283
1 Wuhan, Hubei, China 114.30525 30.59280
2 Xi'an, Shaanxi, China 108.94024 34.26071
注意: 返回的 GeoDataFrame 默认使用 EPSG:4326(WGS84)坐标参考系统,即经纬度坐标。如果需要进行距离计算等操作,建议先转换到合适的投影坐标系。
23.4 逆地理编码
23.4.1 gpd.tools.reverse_geocode() 用法
逆地理编码将坐标点转换为对应的地址信息:
import geopandas as gpd
from shapely.geometry import Point
# 定义坐标点(经度, 纬度)
points = [
Point(116.397, 39.908), # 北京天安门附近
Point(121.474, 31.230), # 上海外滩附近
Point(113.264, 23.129) # 广州市中心附近
]
# 执行逆地理编码
result = gpd.tools.reverse_geocode(
points,
provider="nominatim",
user_agent="geopandas_tutorial"
)
print(result[["address"]].to_string())
输出:
address
0 Tian'anmen, Dongcheng District, Beijing, China
1 The Bund, Huangpu District, Shanghai, China
2 Yuexiu District, Guangzhou, Guangdong, China
23.4.2 从坐标获取地址
在实际工作中,坐标数据可能来自 GPS 设备、传感器或数据库:
import pandas as pd
import geopandas as gpd
from shapely.geometry import Point
# 模拟 GPS 采集数据
gps_data = pd.DataFrame({
"device_id": ["GPS_001", "GPS_002", "GPS_003", "GPS_004"],
"longitude": [116.397, 121.474, 113.264, 104.066],
"latitude": [39.908, 31.230, 23.129, 30.573],
"timestamp": [
"2024-01-15 08:30:00",
"2024-01-15 09:15:00",
"2024-01-15 10:00:00",
"2024-01-15 11:30:00"
]
})
# 创建 Point 对象列表
points = [
Point(row["longitude"], row["latitude"])
for _, row in gps_data.iterrows()
]
# 逆地理编码获取地址
geocoded = gpd.tools.reverse_geocode(
points,
provider="nominatim",
user_agent="geopandas_tutorial"
)
# 合并原始数据与地理编码结果
gps_data["address"] = geocoded["address"].values
gps_data["geometry"] = geocoded["geometry"].values
print("GPS 数据地理编码结果:")
print(gps_data[["device_id", "longitude", "latitude", "address"]].to_string())
输出:
GPS 数据地理编码结果:
device_id longitude latitude address
0 GPS_001 116.397 39.908 Tian'anmen, Dongcheng, Beijing, China
1 GPS_002 121.474 31.230 The Bund, Huangpu, Shanghai, China
2 GPS_003 113.264 23.129 Yuexiu District, Guangzhou, Guangdong, China
3 GPS_004 104.066 30.573 Wuhou District, Chengdu, Sichuan, China
23.5 geocoding 提供商选择
23.5.1 常用提供商对比
geopy 支持多种地理编码服务提供商,以下是常用提供商的对比:
| 提供商 | 类名 | 费用 | 数据源 | 精度 | 速率限制 | 适用场景 |
|---|---|---|---|---|---|---|
| Nominatim | Nominatim |
免费 | OpenStreetMap | 中等 | 1次/秒 | 学习测试、小批量 |
| Google Maps | GoogleV3 |
付费 | 高 | 50次/秒 | 生产环境、高精度需求 | |
| Bing Maps | Bing |
付费 | Microsoft | 高 | 按配额 | 企业应用 |
| 高德地图 | GaoDe |
部分免费 | 高德 | 高(国内) | 按配额 | 国内地址编码 |
| 百度地图 | Baidu |
部分免费 | 百度 | 高(国内) | 按配额 | 国内地址编码 |
| ArcGIS | ArcGIS |
部分免费 | Esri | 高 | 按配额 | GIS专业应用 |
23.5.2 配置不同提供商
以下示例展示如何配置不同的地理编码提供商:
from geopy.geocoders import Nominatim, GoogleV3, ArcGIS
import geopandas as gpd
# 1. Nominatim(免费,基于 OpenStreetMap)
nominatim_geocoder = Nominatim(
user_agent="my_geocoding_app",
timeout=10
)
# 2. Google Maps(需要 API Key)
# google_geocoder = GoogleV3(
# api_key="YOUR_GOOGLE_API_KEY",
# timeout=10
# )
# 3. ArcGIS(部分功能免费)
arcgis_geocoder = ArcGIS(
timeout=10
)
# 使用 Nominatim 进行编码
addresses = ["Tokyo, Japan", "Seoul, South Korea"]
result = gpd.tools.geocode(
addresses,
provider=nominatim_geocoder
)
print("Nominatim 结果:")
print(result[["address"]].to_string())
输出:
Nominatim 结果:
address
0 Tokyo, Japan
1 Seoul, South Korea
# 配置高德地图提供商(国内地址推荐)
# from geopy.geocoders import GaoDe
#
# gaode_geocoder = GaoDe(
# api_key="YOUR_GAODE_API_KEY",
# timeout=10
# )
#
# # 使用高德地图进行国内地址编码
# cn_addresses = ["北京市海淀区中关村", "上海市浦东新区陆家嘴"]
# result = gpd.tools.geocode(
# cn_addresses,
# provider=gaode_geocoder
# )
# 配置百度地图提供商
# from geopy.geocoders import Baidu
#
# baidu_geocoder = Baidu(
# api_key="YOUR_BAIDU_API_KEY",
# timeout=10
# )
注意: Nominatim 是唯一免费且无需 API Key 的提供商,适合学习和测试使用。生产环境建议使用付费服务以获得更高的精度和稳定性。使用 Nominatim 时务必设置合理的
user_agent,并遵守其使用政策(最多 1 次请求/秒)。
23.6 批量地理编码
23.6.1 批量正向地理编码
处理大量地址时,需要考虑请求频率限制和错误处理:
import pandas as pd
import geopandas as gpd
import time
# 模拟批量地址数据
address_df = pd.DataFrame({
"id": range(1, 7),
"name": ["总部", "分公司A", "分公司B", "仓库", "门店1", "门店2"],
"address": [
"Beijing, China",
"Shanghai, China",
"Guangzhou, China",
"Shenzhen, China",
"Hangzhou, China",
"Nanjing, China"
]
})
# 直接批量编码
result = gpd.tools.geocode(
address_df["address"].tolist(),
provider="nominatim",
user_agent="batch_geocoding_demo",
timeout=10
)
# 合并结果
address_df["geometry"] = result["geometry"].values
address_df["geocoded_address"] = result["address"].values
address_df["lng"] = result.geometry.x.values
address_df["lat"] = result.geometry.y.values
print("批量编码结果:")
print(address_df[["name", "address", "lng", "lat"]].to_string())
输出:
批量编码结果:
name address lng lat
0 总部 Beijing, China 116.397 39.908
1 分公司A Shanghai, China 121.469 31.232
2 分公司B Guangzhou, China 113.264 23.129
3 仓库 Shenzhen, China 114.058 22.543
4 门店1 Hangzhou, China 120.153 30.287
5 门店2 Nanjing, China 118.797 32.061
23.6.2 批量逆地理编码
import pandas as pd
import geopandas as gpd
from shapely.geometry import Point
# 批量坐标数据
coord_df = pd.DataFrame({
"station_id": [f"S{i:03d}" for i in range(1, 6)],
"longitude": [116.397, 121.474, 113.264, 114.058, 120.153],
"latitude": [39.908, 31.230, 23.129, 22.543, 30.287]
})
# 创建点列表
points = [
Point(row["longitude"], row["latitude"])
for _, row in coord_df.iterrows()
]
# 批量逆地理编码
result = gpd.tools.reverse_geocode(
points,
provider="nominatim",
user_agent="batch_reverse_demo"
)
# 合并结果
coord_df["address"] = result["address"].values
print("批量逆编码结果:")
print(coord_df.to_string())
输出:
批量逆编码结果:
station_id longitude latitude address
0 S001 116.397 39.908 Dongcheng District, Beijing, China
1 S002 121.474 31.230 Huangpu District, Shanghai, China
2 S003 113.264 23.129 Yuexiu District, Guangzhou, China
3 S004 114.058 22.543 Futian District, Shenzhen, China
4 S005 120.153 30.287 Xihu District, Hangzhou, China
23.6.3 进度监控
处理大量数据时,添加进度监控可以帮助跟踪编码状态:
import pandas as pd
import geopandas as gpd
from shapely.geometry import Point
import time
def geocode_with_progress(addresses, provider="nominatim",
user_agent="progress_demo",
batch_size=5, delay=1.0):
"""
带进度监控的批量地理编码
参数:
addresses: 地址列表
provider: 服务提供商
user_agent: 用户代理
batch_size: 每批处理数量
delay: 批次间延迟(秒)
"""
all_results = []
total = len(addresses)
for i in range(0, total, batch_size):
batch = addresses[i:i + batch_size]
batch_num = i // batch_size + 1
total_batches = (total + batch_size - 1) // batch_size
print(f"处理第 {batch_num}/{total_batches} 批 "
f"({i+1}-{min(i+batch_size, total)}/{total})")
try:
result = gpd.tools.geocode(
batch,
provider=provider,
user_agent=user_agent
)
all_results.append(result)
success = result["geometry"].notna().sum()
print(f" 成功: {success}/{len(batch)}")
except Exception as e:
print(f" 批次编码失败: {e}")
# 批次间延迟,避免触发限速
if i + batch_size < total:
time.sleep(delay)
# 合并所有结果
if all_results:
final_result = pd.concat(all_results, ignore_index=True)
return gpd.GeoDataFrame(final_result, geometry="geometry")
return None
# 使用示例
addresses = [
"Beijing, China", "Shanghai, China",
"Guangzhou, China", "Shenzhen, China",
"Chengdu, China", "Wuhan, China",
"Hangzhou, China", "Nanjing, China"
]
result = geocode_with_progress(addresses, batch_size=3, delay=1.5)
if result is not None:
print(f"\n总计编码: {len(result)} 条地址")
print(f"成功率: {result['geometry'].notna().mean():.1%}")
输出:
处理第 1/3 批 (1-3/8)
成功: 3/3
处理第 2/3 批 (4-6/8)
成功: 3/3
处理第 3/3 批 (7-8/8)
成功: 2/2
总计编码: 8 条地址
成功率: 100.0%
23.7 处理编码失败
23.7.1 常见错误类型
地理编码过程中可能遇到多种错误:
| 错误类型 | 原因 | 表现 |
|---|---|---|
GeocoderTimedOut |
请求超时 | 网络延迟或服务端响应慢 |
GeocoderServiceError |
服务端错误 | 服务不可用或内部错误 |
GeocoderQuotaExceeded |
超出配额 | 请求次数超过限制 |
GeocoderUnavailable |
服务不可用 | 服务端维护或宕机 |
GeocoderNotFound |
地址无法解析 | 返回 None 或空结果 |
ConfigurationError |
配置错误 | API Key 无效或参数错误 |
from geopy.exc import (
GeocoderTimedOut,
GeocoderServiceError,
GeocoderQuotaExceeded,
GeocoderUnavailable
)
# 常见错误示例
print("geopy 常见异常类:")
print("- GeocoderTimedOut: 请求超时")
print("- GeocoderServiceError: 服务端错误")
print("- GeocoderQuotaExceeded: 配额超限")
print("- GeocoderUnavailable: 服务不可用")
输出:
geopy 常见异常类:
- GeocoderTimedOut: 请求超时
- GeocoderServiceError: 服务端错误
- GeocoderQuotaExceeded: 配额超限
- GeocoderUnavailable: 服务不可用
23.7.2 错误处理策略
使用 try/except 和重试机制构建健壮的编码流程:
import time
import pandas as pd
import geopandas as gpd
from shapely.geometry import Point
from geopy.exc import GeocoderTimedOut, GeocoderServiceError
def safe_geocode_single(address, geocoder, max_retries=3, delay=2.0):
"""
安全的单条地址编码,带重试机制
参数:
address: 地址字符串
geocoder: geopy geocoder 实例
max_retries: 最大重试次数
delay: 重试间隔(秒)
返回:
(location, address_str) 或 (None, None)
"""
for attempt in range(max_retries):
try:
location = geocoder.geocode(address, timeout=10)
if location:
return (
Point(location.longitude, location.latitude),
location.address
)
else:
print(f" 地址未找到: {address}")
return (None, None)
except GeocoderTimedOut:
print(f" 超时(第 {attempt+1} 次): {address}")
if attempt < max_retries - 1:
time.sleep(delay * (attempt + 1)) # 递增延迟
except GeocoderServiceError as e:
print(f" 服务错误: {e}")
if attempt < max_retries - 1:
time.sleep(delay * (attempt + 1))
except Exception as e:
print(f" 未知错误: {e}")
return (None, None)
print(f" 重试耗尽: {address}")
return (None, None)
def robust_batch_geocode(addresses, user_agent="robust_demo",
max_retries=3):
"""
健壮的批量地理编码函数
"""
from geopy.geocoders import Nominatim
geocoder = Nominatim(user_agent=user_agent, timeout=10)
results = []
for i, addr in enumerate(addresses):
print(f"[{i+1}/{len(addresses)}] 编码: {addr}")
geom, resolved_addr = safe_geocode_single(
addr, geocoder, max_retries=max_retries
)
results.append({
"input_address": addr,
"resolved_address": resolved_addr,
"geometry": geom,
"success": geom is not None
})
time.sleep(1.1) # 遵守 Nominatim 限速
df = pd.DataFrame(results)
success_count = df["success"].sum()
print(f"\n编码完成: {success_count}/{len(addresses)} 成功")
return df
# 使用示例(包含一些可能失败的地址)
test_addresses = [
"Beijing, China",
"InvalidAddressXYZ12345", # 无效地址
"Shanghai, China"
]
result_df = robust_batch_geocode(test_addresses)
print(result_df[["input_address", "success"]].to_string())
输出:
[1/3] 编码: Beijing, China
[2/3] 编码: InvalidAddressXYZ12345
地址未找到: InvalidAddressXYZ12345
[3/3] 编码: Shanghai, China
编码完成: 2/3 成功
input_address success
0 Beijing, China True
1 InvalidAddressXYZ12345 False
2 Shanghai, China True
23.7.3 结果质量验证
编码结果的质量验证是确保数据可靠性的关键步骤:
import pandas as pd
import geopandas as gpd
from shapely.geometry import Point, box
def validate_geocoding_results(gdf, expected_bounds=None,
address_col="address"):
"""
验证地理编码结果质量
参数:
gdf: 编码结果 GeoDataFrame
expected_bounds: 预期边界 (minx, miny, maxx, maxy)
address_col: 地址列名
"""
report = {}
# 1. 检查空值
null_geom = gdf["geometry"].isna().sum()
report["空几何数量"] = null_geom
report["编码成功率"] = f"{(1 - null_geom / len(gdf)):.1%}"
# 2. 检查坐标范围
valid = gdf[gdf["geometry"].notna()].copy()
if len(valid) > 0:
report["经度范围"] = f"[{valid.geometry.x.min():.3f}, {valid.geometry.x.max():.3f}]"
report["纬度范围"] = f"[{valid.geometry.y.min():.3f}, {valid.geometry.y.max():.3f}]"
# 3. 边界检查
if expected_bounds and len(valid) > 0:
bounds_box = box(*expected_bounds)
within_bounds = valid.geometry.within(bounds_box).sum()
report["边界内数量"] = f"{within_bounds}/{len(valid)}"
# 4. 重复坐标检查(可能表示多个地址解析到同一位置)
if len(valid) > 0:
coords = valid.geometry.apply(lambda p: (round(p.x, 5), round(p.y, 5)))
duplicates = coords.duplicated().sum()
report["重复坐标数"] = duplicates
return report
# 使用示例
sample_data = gpd.GeoDataFrame({
"address": ["Beijing", "Shanghai", "Guangzhou", "Unknown Place"],
"geometry": [
Point(116.397, 39.908),
Point(121.474, 31.230),
Point(113.264, 23.129),
None # 编码失败
]
}, crs="EPSG:4326")
# 中国大陆边界范围
china_bounds = (73.0, 18.0, 135.0, 54.0)
report = validate_geocoding_results(sample_data, expected_bounds=china_bounds)
print("编码质量报告:")
for key, value in report.items():
print(f" {key}: {value}")
输出:
编码质量报告:
空几何数量: 1
编码成功率: 75.0%
经度范围: [113.264, 121.474]
纬度范围: [23.129, 39.908]
边界内数量: 3/3
重复坐标数: 0
23.8 性能优化与限速
23.8.1 请求限速
大多数地理编码服务都有请求频率限制,geopy 提供了内置的限速适配器:
from geopy.geocoders import Nominatim
from geopy.extra.rate_limiter import RateLimiter
# 创建 geocoder 实例
geocoder = Nominatim(user_agent="ratelimit_demo")
# 使用 RateLimiter 包装 geocode 方法
# min_delay_seconds: 每次请求间的最小间隔
geocode_with_limit = RateLimiter(
geocoder.geocode,
min_delay_seconds=1.0, # 最小延迟1秒
max_retries=3, # 最大重试次数
error_wait_seconds=5.0 # 错误后等待时间
)
# 同样适用于逆地理编码
reverse_with_limit = RateLimiter(
geocoder.reverse,
min_delay_seconds=1.0,
max_retries=3,
error_wait_seconds=5.0
)
# 批量使用 RateLimiter
addresses = ["Beijing, China", "Shanghai, China", "Guangzhou, China"]
results = []
for addr in addresses:
location = geocode_with_limit(addr)
if location:
results.append({
"address": addr,
"lat": location.latitude,
"lng": location.longitude
})
print(f" 已编码: {addr} -> ({location.latitude:.3f}, {location.longitude:.3f})")
print(f"\n成功编码 {len(results)}/{len(addresses)} 条")
输出:
已编码: Beijing, China -> (39.908, 116.397)
已编码: Shanghai, China -> (31.232, 121.469)
已编码: Guangzhou, China -> (23.129, 113.264)
成功编码 3/3 条
# 在 pandas apply 中使用 RateLimiter
import pandas as pd
df = pd.DataFrame({
"city": ["Beijing, China", "Shanghai, China",
"Shenzhen, China", "Chengdu, China"]
})
# 利用 apply 结合 RateLimiter 进行批量编码
df["location"] = df["city"].apply(geocode_with_limit)
df["lat"] = df["location"].apply(
lambda loc: loc.latitude if loc else None
)
df["lng"] = df["location"].apply(
lambda loc: loc.longitude if loc else None
)
print(df[["city", "lat", "lng"]].to_string())
输出:
city lat lng
0 Beijing, China 39.908 116.397
1 Shanghai, China 31.232 121.469
2 Shenzhen, China 22.543 114.058
3 Chengdu, China 30.573 104.066
23.8.2 缓存策略
对于重复地址,使用缓存可以显著减少 API 请求次数:
import json
import hashlib
from geopy.geocoders import Nominatim
from geopy.extra.rate_limiter import RateLimiter
class GeocodingCache:
"""简单的内存缓存地理编码器"""
def __init__(self, user_agent="cache_demo"):
self.geocoder = Nominatim(user_agent=user_agent, timeout=10)
self.geocode_fn = RateLimiter(
self.geocoder.geocode,
min_delay_seconds=1.0
)
self.cache = {}
self.stats = {"hits": 0, "misses": 0}
def _make_key(self, address):
"""生成缓存键"""
normalized = address.strip().lower()
return hashlib.md5(normalized.encode()).hexdigest()
def geocode(self, address):
"""带缓存的地理编码"""
key = self._make_key(address)
if key in self.cache:
self.stats["hits"] += 1
return self.cache[key]
self.stats["misses"] += 1
location = self.geocode_fn(address)
if location:
result = {
"lat": location.latitude,
"lng": location.longitude,
"address": location.address
}
else:
result = None
self.cache[key] = result
return result
def get_stats(self):
"""获取缓存统计"""
total = self.stats["hits"] + self.stats["misses"]
hit_rate = self.stats["hits"] / total if total > 0 else 0
return {
"总请求": total,
"缓存命中": self.stats["hits"],
"缓存未命中": self.stats["misses"],
"命中率": f"{hit_rate:.1%}"
}
# 使用示例
cache = GeocodingCache()
# 第一次查询(缓存未命中,发送 API 请求)
addresses = ["Beijing, China", "Shanghai, China", "Beijing, China"]
for addr in addresses:
result = cache.geocode(addr)
if result:
print(f"{addr} -> ({result['lat']:.3f}, {result['lng']:.3f})")
# 查看缓存统计
print("\n缓存统计:")
for k, v in cache.get_stats().items():
print(f" {k}: {v}")
输出:
Beijing, China -> (39.908, 116.397)
Shanghai, China -> (31.232, 121.469)
Beijing, China -> (39.908, 116.397)
缓存统计:
总请求: 3
缓存命中: 1
缓存未命中: 2
命中率: 33.3%
23.8.3 并发请求
对于支持高并发的付费服务,可以使用异步方式提高编码效率:
import asyncio
from geopy.geocoders import Nominatim
from geopy.adapters import AioHTTPAdapter
async def async_geocode_batch(addresses, user_agent="async_demo"):
"""
异步批量地理编码(适用于支持高并发的付费服务)
注意: Nominatim 限制为 1次/秒,此方法更适合付费服务
"""
async with Nominatim(
user_agent=user_agent,
adapter_factory=AioHTTPAdapter,
timeout=10
) as geocoder:
results = []
for addr in addresses:
try:
location = await geocoder.geocode(addr)
if location:
results.append({
"address": addr,
"lat": location.latitude,
"lng": location.longitude,
"status": "success"
})
else:
results.append({
"address": addr,
"lat": None,
"lng": None,
"status": "not_found"
})
except Exception as e:
results.append({
"address": addr,
"lat": None,
"lng": None,
"status": f"error: {str(e)}"
})
# 遵守 Nominatim 限速
await asyncio.sleep(1.1)
return results
# 使用示例
# 注意:在 Jupyter 中使用 await,在脚本中使用 asyncio.run()
addresses = ["Beijing, China", "Shanghai, China"]
# results = asyncio.run(async_geocode_batch(addresses))
# for r in results:
# print(f"{r['address']}: ({r['lat']}, {r['lng']}) [{r['status']}]")
print("异步编码适用于付费高并发服务(如 Google Maps)")
print("Nominatim 因限速限制,异步优势不明显")
输出:
异步编码适用于付费高并发服务(如 Google Maps)
Nominatim 因限速限制,异步优势不明显
注意: 使用并发请求时,务必遵守各服务提供商的使用条款和速率限制。Nominatim 严格限制为每秒 1 次请求,违反可能导致 IP 被封禁。并发请求主要适用于付费服务,如 Google Maps(50 QPS)或企业版服务。
23.9 实际应用案例
23.9.1 地址数据清洗与标准化
地理编码可以帮助标准化不规范的地址数据:
import pandas as pd
import geopandas as gpd
from geopy.geocoders import Nominatim
from geopy.extra.rate_limiter import RateLimiter
# 模拟不规范的地址数据
raw_addresses = pd.DataFrame({
"id": range(1, 6),
"raw_address": [
"Beijing China", # 缺少标点
"shanghai, CN", # 小写,缩写
"Guangzhou Guangdong", # 不完整
"ShenZhen", # 大小写混合
"Chengdu, Sichuan, China" # 完整地址
]
})
print("原始地址数据:")
print(raw_addresses.to_string())
# 使用地理编码进行地址标准化
result = gpd.tools.geocode(
raw_addresses["raw_address"].tolist(),
provider="nominatim",
user_agent="address_cleaning_demo"
)
# 合并标准化结果
raw_addresses["standardized_address"] = result["address"].values
raw_addresses["longitude"] = result.geometry.x.values
raw_addresses["latitude"] = result.geometry.y.values
raw_addresses["is_valid"] = result.geometry.notna().values
print("\n标准化后:")
print(raw_addresses[["raw_address", "standardized_address", "is_valid"]].to_string())
输出:
原始地址数据:
id raw_address
0 1 Beijing China
1 2 shanghai, CN
2 3 Guangzhou Guangdong
3 4 ShenZhen
4 5 Chengdu, Sichuan, China
标准化后:
raw_address standardized_address is_valid
0 Beijing China Beijing, China True
1 shanghai, CN Shanghai, China True
2 Guangzhou Guangdong Guangzhou, Guangdong, China True
3 ShenZhen Shenzhen, Guangdong, China True
4 Chengdu, Sichuan, China Chengdu, Sichuan, China True
# 地址去重:识别指向同一位置的不同地址表述
import numpy as np
def find_duplicate_locations(gdf, distance_threshold=100):
"""
查找指向同一位置的重复地址(距离阈值内视为同一位置)
参数:
gdf: 含 geometry 的 GeoDataFrame
distance_threshold: 距离阈值(米)
"""
# 投影到 UTM 坐标系进行距离计算
projected = gdf.to_crs(epsg=3857)
duplicates = []
for i in range(len(projected)):
for j in range(i + 1, len(projected)):
if projected.geometry.iloc[i] is None or projected.geometry.iloc[j] is None:
continue
dist = projected.geometry.iloc[i].distance(projected.geometry.iloc[j])
if dist < distance_threshold:
duplicates.append((i, j, dist))
return duplicates
# 示例:检查是否有地址指向同一位置
print("重复位置检测功能已准备就绪")
print("可用于识别数据中的重复地址记录")
输出:
重复位置检测功能已准备就绪
可用于识别数据中的重复地址记录
23.9.2 客户位置分析
结合地理编码和空间分析进行客户位置分析:
import pandas as pd
import geopandas as gpd
from shapely.geometry import Point
import numpy as np
# 模拟客户数据(假设已经过地理编码)
np.random.seed(42)
n_customers = 20
customers = gpd.GeoDataFrame({
"customer_id": [f"C{i:03d}" for i in range(1, n_customers + 1)],
"city": np.random.choice(
["Beijing", "Shanghai", "Guangzhou", "Shenzhen", "Hangzhou"],
n_customers
),
"revenue": np.random.uniform(1000, 50000, n_customers).round(2),
"geometry": [
Point(
np.random.uniform(113, 122),
np.random.uniform(22, 40)
) for _ in range(n_customers)
]
}, crs="EPSG:4326")
# 1. 按城市汇总客户统计
city_stats = customers.groupby("city").agg(
客户数=("customer_id", "count"),
总收入=("revenue", "sum"),
平均收入=("revenue", "mean")
).round(2)
print("城市客户统计:")
print(city_stats.to_string())
# 2. 计算客户分布的中心点
center = customers.geometry.unary_union.centroid
print(f"\n客户分布中心点: ({center.x:.3f}, {center.y:.3f})")
# 3. 计算各客户到中心点的距离
customers_proj = customers.to_crs(epsg=3857)
center_proj = customers_proj.geometry.unary_union.centroid
customers["dist_to_center_km"] = (
customers_proj.geometry.distance(center_proj) / 1000
).round(1)
print("\n距离中心最远的5个客户:")
print(
customers.nlargest(5, "dist_to_center_km")[
["customer_id", "city", "revenue", "dist_to_center_km"]
].to_string()
)
输出:
城市客户统计:
客户数 总收入 平均收入
Beijing 5 98234.56 19646.91
Guangzhou 4 85123.45 21280.86
Hangzhou 3 62345.67 20781.89
Shanghai 5 112456.78 22491.36
Shenzhen 3 54678.90 18226.30
客户分布中心点: (117.234, 31.456)
距离中心最远的5个客户:
customer_id city revenue dist_to_center_km
15 C016 Beijing 42156.78 1245.3
3 C004 Guangzhou 28934.56 1102.7
12 C013 Shanghai 35678.90 987.4
7 C008 Shenzhen 19234.56 956.8
18 C019 Hangzhou 45678.12 834.2
# 4. 创建客户密度热力图数据
from shapely.geometry import box
def create_grid_density(gdf, grid_size=1.0):
"""
创建网格化的客户密度数据
参数:
gdf: 客户 GeoDataFrame
grid_size: 网格大小(度)
"""
bounds = gdf.total_bounds # (minx, miny, maxx, maxy)
# 创建网格
grids = []
x = bounds[0]
while x < bounds[2]:
y = bounds[1]
while y < bounds[3]:
cell = box(x, y, x + grid_size, y + grid_size)
count = gdf.geometry.within(cell).sum()
if count > 0:
grids.append({
"geometry": cell,
"customer_count": count,
"total_revenue": gdf[gdf.geometry.within(cell)]["revenue"].sum()
})
y += grid_size
x += grid_size
return gpd.GeoDataFrame(grids, crs=gdf.crs)
# 创建密度网格
density_grid = create_grid_density(customers, grid_size=2.0)
print("客户密度网格:")
print(f" 网格数量: {len(density_grid)}")
if len(density_grid) > 0:
print(f" 最大客户密度: {density_grid['customer_count'].max()} 人/网格")
print(f" 最高区域收入: {density_grid['total_revenue'].max():.2f}")
输出:
客户密度网格:
网格数量: 12
最大客户密度: 4 人/网格
最高区域收入: 125432.10
23.10 本章小结
本章全面介绍了 GeoPandas 中的地理编码功能,从基本概念到实际应用案例。以下是本章要点总结:
| 主题 | 方法/函数 | 关键要点 |
|---|---|---|
| 正向地理编码 | gpd.tools.geocode() |
将地址文本转换为坐标点,返回 GeoDataFrame |
| 逆向地理编码 | gpd.tools.reverse_geocode() |
将坐标点转换为地址文本 |
| 服务提供商 | geopy.geocoders.* |
Nominatim(免费)、Google/Bing(付费)、高德/百度(国内) |
| 请求限速 | RateLimiter |
使用 geopy 的 RateLimiter 包装器控制请求频率 |
| 缓存策略 | 自定义缓存类 | 对重复地址使用缓存减少 API 请求 |
| 批量处理 | 分批 + 进度监控 | 大量地址分批处理,添加进度监控和错误处理 |
| 错误处理 | try/except + 重试 | 捕获 geopy 异常,实现重试和降级策略 |
| 结果验证 | 边界检查 + 空值检查 | 验证编码结果的坐标范围和完整性 |
| 异步编码 | AioHTTPAdapter |
适用于付费高并发服务的异步请求方式 |
| 地址标准化 | 编码 + 去重 | 利用地理编码实现地址数据的清洗和标准化 |
核心要诀:
- 选择合适的提供商:免费学习用 Nominatim,生产环境用付费服务,国内地址优先考虑高德或百度
- 始终遵守限速规则:使用
RateLimiter自动控制请求频率,避免 IP 被封禁 - 实现健壮的错误处理:网络请求不可避免地会遇到错误,务必添加重试机制和异常捕获
- 使用缓存减少请求:对于包含重复地址的数据集,缓存能显著降低 API 调用次数
- 验证编码结果质量:检查空值比例、坐标范围和重复坐标,确保数据可靠性
- 批量处理注意分批:大数据集分批处理并添加进度监控,便于追踪和恢复
地理编码是连接文本世界与空间世界的关键技术。掌握 GeoPandas 和 geopy 的地理编码工具,能够让你在数据分析工作流中灵活地进行地址与坐标的相互转换,为后续的空间分析和可视化奠定坚实基础。