Day 9: 地理数据分析
学习目标
完成今天的学习后,你将能够:
- 理解地理数据类型
- 进行地图可视化
- 进行空间分析
- 掌握地理编码
技术原理
地理数据类型
矢量数据
- 点(Point):坐标位置
- 线(Line):路径、道路
- 面(Polygon):区域、边界
栅格数据
- 图像:卫星图像、航拍图像
- 网格:规则网格数据
坐标系统
地理坐标系
- 经度:东西位置
- 纬度:南北位置
- WGS84:全球通用坐标系
投影坐标系
- UTM:通用横轴墨卡托投影
- 高斯-克吕格:中国常用投影
空间分析
基本操作
- 距离计算:两点之间的距离
- 缓冲区分析:创建缓冲区
- 叠加分析:图层叠加
- 空间查询:基于位置的查询
高级分析
- 空间聚类:DBSCAN、K-means
- 空间插值:克里金插值、反距离加权
- 网络分析:路径规划、服务区分析
地理编码
正向地理编码
将地址转换为坐标
反向地理编码
将坐标转换为地址
案例:城市设施分析
假设我们有一个城市设施数据集,需要:
- 可视化设施分布
- 分析设施覆盖范围
- 计算设施之间的距离
- 进行空间聚类
应用场景
- 城市规划:设施布局、交通规划
- 商业分析:选址分析、市场覆盖
- 环境监测:污染分布、生态分析
- 物流配送:路径优化、配送范围
- 应急管理:灾害评估、救援规划
代码案例
地理数据处理
python
# 地理数据处理示例
import pandas as pd
import numpy as np
import geopandas as gpd
import matplotlib.pyplot as plt
from shapely.geometry import Point, LineString, Polygon
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# 1. 创建点数据
# 创建一些城市坐标
cities = {
'城市': ['北京', '上海', '广州', '深圳', '成都', '杭州', '武汉', '南京'],
'经度': [116.4074, 121.4737, 113.2644, 114.0579, 104.0665, 120.1551, 114.3055, 118.7969],
'纬度': [39.9042, 31.2304, 23.1291, 22.5431, 30.5728, 30.2741, 30.5928, 32.0603],
'人口(万)': [2154, 2487, 1868, 1756, 2094, 1220, 1233, 931]
}
df = pd.DataFrame(cities)
# 创建几何对象
geometry = [Point(xy) for xy in zip(df['经度'], df['纬度'])]
# 创建GeoDataFrame
gdf = gpd.GeoDataFrame(df, geometry=geometry)
print("点数据:")
print(gdf)
# 2. 创建线数据
# 创建城市之间的连线
lines = []
line_data = []
for i in range(len(gdf)):
for j in range(i + 1, len(gdf)):
line = LineString([gdf.geometry.iloc[i], gdf.geometry.iloc[j]])
lines.append(line)
line_data.append({
'起点': gdf['城市'].iloc[i],
'终点': gdf['城市'].iloc[j]
})
line_df = pd.DataFrame(line_data)
line_gdf = gpd.GeoDataFrame(line_df, geometry=lines)
print("\n线数据:")
print(line_gdf.head())
# 3. 创建面数据
# 创建一个简单的区域
polygon = Polygon([(110, 20), (125, 20), (125, 42), (110, 42)])
area_gdf = gpd.GeoDataFrame([{'名称': '研究区域'}], geometry=[polygon])
print("\n面数据:")
print(area_gdf)
# 4. 可视化
fig, ax = plt.subplots(1, 1, figsize=(12, 8))
# 绘制区域
area_gdf.plot(ax=ax, alpha=0.3, color='lightblue')
# 绘制连线
line_gdf.plot(ax=ax, color='gray', linewidth=0.5, alpha=0.5)
# 绘制城市点
gdf.plot(ax=ax, color='red', markersize=gdf['人口(万)'] / 10, alpha=0.7)
# 添加城市标签
for idx, row in gdf.iterrows():
ax.annotate(row['城市'], (row.geometry.x, row.geometry.y),
xytext=(5, 5), textcoords='offset points',
fontsize=9, fontweight='bold')
ax.set_title('中国主要城市分布', fontsize=16, fontweight='bold')
ax.set_xlabel('经度')
ax.set_ylabel('纬度')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# 5. 基本空间操作
print("\n空间操作:")
print("=" * 50)
# 计算两点之间的距离(单位:度)
point1 = gdf.geometry.iloc[0] # 北京
point2 = gdf.geometry.iloc[1] # 上海
distance = point1.distance(point2)
print(f"北京到上海的距离(度): {distance:.4f}")
# 计算所有城市到北京的距离
beijing = gdf.geometry.iloc[0]
gdf['到北京距离'] = gdf.geometry.apply(lambda x: x.distance(beijing))
print("\n各城市到北京的距离:")
print(gdf[['城市', '到北京距离']])
# 6. 缓冲区分析
# 创建缓冲区
gdf['缓冲区'] = gdf.geometry.buffer(2) # 2度的缓冲区
# 可视化缓冲区
fig, ax = plt.subplots(1, 1, figsize=(12, 8))
# 绘制缓冲区
gdf['缓冲区'].plot(ax=ax, alpha=0.3, color='skyblue')
# 绘制城市点
gdf.plot(ax=ax, color='red', markersize=100)
ax.set_title('城市缓冲区分析', fontsize=16, fontweight='bold')
ax.set_xlabel('经度')
ax.set_ylabel('纬度')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()地图可视化
python
# 地图可视化示例
import pandas as pd
import numpy as np
import geopandas as gpd
import matplotlib.pyplot as plt
import folium
from folium import plugins
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# 1. 创建示例数据
np.random.seed(42)
n_points = 100
# 生成随机点(模拟北京市内的设施)
lons = np.random.uniform(116.0, 116.8, n_points)
lats = np.random.uniform(39.7, 40.2, n_points)
values = np.random.uniform(0, 100, n_points)
categories = np.random.choice(['商业', '教育', '医疗', '交通'], n_points)
df = pd.DataFrame({
'经度': lons,
'纬度': lats,
'值': values,
'类别': categories
})
geometry = [Point(xy) for xy in zip(df['经度'], df['纬度'])]
gdf = gpd.GeoDataFrame(df, geometry=geometry)
print("设施数据:")
print(gdf.head())
# 2. 使用matplotlib绘制地图
fig, axes = plt.subplots(1, 2, figsize=(16, 6))
# 按类别着色
colors = {'商业': 'red', '教育': 'blue', '医疗': 'green', '交通': 'orange'}
for category, color in colors.items():
mask = gdf['类别'] == category
gdf[mask].plot(ax=axes[0], color=color, markersize=50, alpha=0.6, label=category)
axes[0].set_title('按类别分布', fontsize=14, fontweight='bold')
axes[0].set_xlabel('经度')
axes[0].set_ylabel('纬度')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# 按值着色
gdf.plot(ax=axes[1], column='值', cmap='YlOrRd', markersize=50, alpha=0.6, legend=True)
axes[1].set_title('按值分布', fontsize=14, fontweight='bold')
axes[1].set_xlabel('经度')
axes[1].set_ylabel('纬度')
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# 3. 使用folium创建交互式地图
# 创建基础地图
m = folium.Map(location=[39.9, 116.4], zoom_start=10, tiles='OpenStreetMap')
# 添加标记
for idx, row in gdf.iterrows():
folium.CircleMarker(
location=[row['纬度'], row['经度']],
radius=5,
color=colors.get(row['类别'], 'gray'),
fill=True,
fill_color=colors.get(row['类别'], 'gray'),
fill_opacity=0.7,
popup=f"类别: {row['类别']}<br>值: {row['值']:.2f}"
).add_to(m)
# 添加热力图
heat_data = [[row['纬度'], row['经度']] for idx, row in gdf.iterrows()]
plugins.HeatMap(heat_data).add_to(m)
# 保存地图
m.save('beijing_facilities.html')
print("\n交互式地图已保存为 beijing_facilities.html")
# 4. 创建等值线图
# 创建网格数据
x = np.linspace(116.0, 116.8, 50)
y = np.linspace(39.7, 40.2, 50)
X, Y = np.meshgrid(x, y)
# 模拟某种密度值
Z = np.sin(X * 10) * np.cos(Y * 10) + np.random.normal(0, 0.1, X.shape)
fig, ax = plt.subplots(1, 1, figsize=(10, 8))
contour = ax.contourf(X, Y, Z, levels=20, cmap='viridis', alpha=0.7)
plt.colorbar(contour, ax=ax, label='密度值')
ax.set_title('等值线图', fontsize=16, fontweight='bold')
ax.set_xlabel('经度')
ax.set_ylabel('纬度')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# 5. 创建 choropleth 地图
# 创建区域数据
regions = {
'区域': ['东城区', '西城区', '朝阳区', '海淀区', '丰台区', '石景山区'],
'经度': [116.418, 116.366, 116.486, 116.311, 116.287, 116.224],
'纬度': [39.917, 39.912, 39.921, 39.957, 39.863, 39.906],
'人口(万)': [82, 110, 345, 328, 212, 62],
'GDP(亿)': [2000, 3000, 5000, 6000, 1500, 500]
}
region_df = pd.DataFrame(regions)
region_geometry = [Point(xy) for xy in zip(region_df['经度'], region_df['纬度'])]
region_gdf = gpd.GeoDataFrame(region_df, geometry=region_geometry)
# 可视化
fig, axes = plt.subplots(1, 2, figsize=(16, 6))
# 人口分布
region_gdf.plot(ax=axes[0], column='人口(万)', cmap='YlOrRd', markersize=region_gdf['人口(万)'] * 2, legend=True)
axes[0].set_title('人口分布', fontsize=14, fontweight='bold')
axes[0].set_xlabel('经度')
axes[0].set_ylabel('纬度')
# GDP分布
region_gdf.plot(ax=axes[1], column='GDP(亿)', cmap='YlGnBu', markersize=region_gdf['GDP(亿)'] / 10, legend=True)
axes[1].set_title('GDP分布', fontsize=14, fontweight='bold')
axes[1].set_xlabel('经度')
axes[1].set_ylabel('纬度')
plt.tight_layout()
plt.show()空间分析
python
# 空间分析示例
import pandas as pd
import numpy as np
import geopandas as gpd
import matplotlib.pyplot as plt
from shapely.geometry import Point, Polygon
from scipy.spatial.distance import cdist
from sklearn.cluster import DBSCAN
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# 1. 创建示例数据
np.random.seed(42)
n_points = 200
# 生成三个聚类中心
centers = [(116.4, 39.9), (116.6, 40.0), (116.5, 39.8)]
points = []
for center in centers:
cluster_points = np.random.normal(center, 0.05, (n_points // 3, 2))
points.extend(cluster_points)
points = np.array(points)
df = pd.DataFrame({'经度': points[:, 0], '纬度': points[:, 1]})
geometry = [Point(xy) for xy in zip(df['经度'], df['纬度'])]
gdf = gpd.GeoDataFrame(df, geometry=geometry)
print("原始数据:")
print(gdf.head())
# 2. DBSCAN空间聚类
# 将经纬度转换为弧度(用于DBSCAN)
coords_rad = np.radians(gdf[['纬度', '经度']].values)
# 使用DBSCAN进行聚类
# eps是邻域半径(单位:弧度),min_samples是最小样本数
db = DBSCAN(eps=0.01, min_samples=5).fit(coords_rad)
gdf['聚类标签'] = db.labels_
print("\n聚类结果:")
print(gdf['聚类标签'].value_counts())
# 可视化聚类结果
fig, ax = plt.subplots(1, 1, figsize=(10, 8))
# 绘制不同聚类的点
unique_labels = set(gdf['聚类标签'])
colors = plt.cm.Set1(np.linspace(0, 1, len(unique_labels)))
for label, color in zip(unique_labels, colors):
if label == -1:
# 噪声点
color = 'gray'
marker = 'x'
else:
marker = 'o'
mask = gdf['聚类标签'] == label
gdf[mask].plot(ax=ax, color=color, markersize=50, alpha=0.6,
label=f'聚类 {label}' if label != -1 else '噪声', marker=marker)
ax.set_title('DBSCAN空间聚类', fontsize=16, fontweight='bold')
ax.set_xlabel('经度')
ax.set_ylabel('纬度')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# 3. 距离矩阵计算
# 计算所有点之间的距离矩阵
coords = gdf[['经度', '纬度']].values
distance_matrix = cdist(coords, coords, metric='euclidean')
print("\n距离矩阵形状:", distance_matrix.shape)
print("平均距离:", np.mean(distance_matrix))
print("最大距离:", np.max(distance_matrix))
print("最小距离:", np.min(distance_matrix))
# 可视化距离矩阵
plt.figure(figsize=(10, 8))
plt.imshow(distance_matrix, cmap='hot', interpolation='nearest')
plt.colorbar(label='距离')
plt.title('距离矩阵', fontsize=16, fontweight='bold')
plt.xlabel('点索引')
plt.ylabel('点索引')
plt.show()
# 4. 缓冲区分析
# 创建缓冲区
gdf['缓冲区'] = gdf.geometry.buffer(0.02) # 0.02度的缓冲区
# 计算缓冲区重叠
from shapely.ops import unary_union
# 合并所有缓冲区
union_buffer = unary_union(gdf['缓冲区'].tolist())
print(f"\n合并后的缓冲区面积: {union_buffer.area:.6f} 平方度")
# 可视化缓冲区
fig, ax = plt.subplots(1, 1, figsize=(10, 8))
# 绘制合并后的缓冲区
gpd.GeoSeries([union_buffer]).plot(ax=ax, alpha=0.3, color='skyblue')
# 绘制原始点
gdf.plot(ax=ax, color='red', markersize=50, alpha=0.7)
ax.set_title('缓冲区分析', fontsize=16, fontweight='bold')
ax.set_xlabel('经度')
ax.set_ylabel('纬度')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# 5. 最近邻分析
from scipy.spatial import KDTree
# 创建KD树
tree = KDTree(coords)
# 查询每个点的最近邻
distances, indices = tree.query(coords, k=2) # k=2包括自身
nearest_distances = distances[:, 1] # 排除自身
print(f"\n最近邻距离统计:")
print(f"平均最近邻距离: {np.mean(nearest_distances):.6f}")
print(f"最大最近邻距离: {np.max(nearest_distances):.6f}")
print(f"最小最近邻距离: {np.min(nearest_distances):.6f}")
# 可视化最近邻距离
plt.figure(figsize=(10, 6))
plt.hist(nearest_distances, bins=30, edgecolor='black', alpha=0.7, color='skyblue')
plt.title('最近邻距离分布', fontsize=16, fontweight='bold')
plt.xlabel('距离')
plt.ylabel('频数')
plt.grid(True, alpha=0.3)
plt.show()
# 6. 空间插值(反距离加权法)
def idw_interpolation(x, y, values, xi, yi, power=2):
"""反距离加权插值"""
distances = np.sqrt((x - xi)**2 + (y - yi)**2)
# 避免除零错误
distances = np.where(distances == 0, 1e-10, distances)
weights = 1 / distances**power
interpolated_value = np.sum(weights * values) / np.sum(weights)
return interpolated_value
# 创建插值网格
x_min, x_max = gdf['经度'].min() - 0.05, gdf['经度'].max() + 0.05
y_min, y_max = gdf['纬度'].min() - 0.05, gdf['纬度'].max() + 0.05
xi = np.linspace(x_min, x_max, 50)
yi = np.linspace(y_min, y_max, 50)
XI, YI = np.meshgrid(xi, yi)
# 为每个点分配随机值
gdf['随机值'] = np.random.uniform(0, 100, len(gdf))
# 进行插值
ZI = np.zeros_like(XI)
for i in range(len(xi)):
for j in range(len(yi)):
ZI[j, i] = idw_interpolation(
gdf['经度'].values,
gdf['纬度'].values,
gdf['随机值'].values,
xi[i],
yi[j]
)
# 可视化插值结果
plt.figure(figsize=(10, 8))
plt.contourf(XI, YI, ZI, levels=20, cmap='viridis', alpha=0.7)
plt.colorbar(label='插值值')
plt.scatter(gdf['经度'], gdf['纬度'], c='red', s=50, edgecolors='black', label='观测点')
plt.title('反距离加权插值', fontsize=16, fontweight='bold')
plt.xlabel('经度')
plt.ylabel('纬度')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()地理编码
python
# 地理编码示例
import pandas as pd
import numpy as np
import geopandas as gpd
import matplotlib.pyplot as plt
from geopy.geocoders import Nominatim
from geopy.distance import geodesic
import time
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# 1. 地理编码函数
def geocode_address(address):
"""将地址转换为坐标"""
geolocator = Nominatim(user_agent="my_geocoder")
try:
location = geolocator.geocode(address)
if location:
return location.latitude, location.longitude
else:
return None, None
except Exception as e:
print(f"地理编码错误: {e}")
return None, None
# 2. 反向地理编码函数
def reverse_geocode(lat, lon):
"""将坐标转换为地址"""
geolocator = Nominatim(user_agent="my_geocoder")
try:
location = geolocator.reverse((lat, lon))
if location:
return location.address
else:
return None
except Exception as e:
print(f"反向地理编码错误: {e}")
return None
# 3. 测试地理编码
print("地理编码测试:")
print("=" * 50)
# 注意:由于网络限制,这里使用模拟数据
# 实际使用时需要网络连接
# 模拟地理编码结果
addresses = [
"北京市天安门广场",
"上海市东方明珠",
"广州市广州塔",
"深圳市市民中心",
"成都市天府广场"
]
# 模拟坐标
coords = [
(39.9042, 116.4074), # 天安门
(31.2397, 121.4998), # 东方明珠
(23.1066, 113.3245), # 广州塔
(22.5431, 114.0579), # 市民中心
(30.5728, 104.0665) # 天府广场
]
# 创建DataFrame
df = pd.DataFrame({
'地址': addresses,
'纬度': [c[0] for c in coords],
'经度': [c[1] for c in coords]
})
print("地理编码结果:")
print(df)
# 4. 计算距离矩阵
print("\n距离矩阵(单位:公里):")
print("=" * 50)
# 计算所有点之间的距离
n = len(df)
distance_matrix = np.zeros((n, n))
for i in range(n):
for j in range(n):
if i != j:
point1 = (df['纬度'].iloc[i], df['经度'].iloc[i])
point2 = (df['纬度'].iloc[j], df['经度'].iloc[j])
distance_matrix[i, j] = geodesic(point1, point2).kilometers
# 创建距离DataFrame
distance_df = pd.DataFrame(distance_matrix, index=df['地址'], columns=df['地址'])
print(distance_df.round(2))
# 5. 可视化距离矩阵
plt.figure(figsize=(10, 8))
plt.imshow(distance_matrix, cmap='YlOrRd', interpolation='nearest')
plt.colorbar(label='距离 (公里)')
plt.xticks(range(n), df['地址'], rotation=45, ha='right')
plt.yticks(range(n), df['地址'])
plt.title('城市间距离矩阵', fontsize=16, fontweight='bold')
# 在矩阵中显示数值
for i in range(n):
for j in range(n):
plt.text(j, i, f'{distance_matrix[i, j]:.0f}',
ha='center', va='center', fontsize=8)
plt.tight_layout()
plt.show()
# 6. 创建GeoDataFrame
geometry = [Point(xy) for xy in zip(df['经度'], df['纬度'])]
gdf = gpd.GeoDataFrame(df, geometry=geometry)
# 7. 可视化地图
fig, ax = plt.subplots(1, 1, figsize=(12, 8))
# 绘制点
gdf.plot(ax=ax, color='red', markersize=100, alpha=0.7)
# 添加标签
for idx, row in gdf.iterrows():
ax.annotate(row['地址'], (row.geometry.x, row.geometry.y),
xytext=(5, 5), textcoords='offset points',
fontsize=9, fontweight='bold')
# 添加连线
for i in range(len(gdf)):
for j in range(i + 1, len(gdf)):
ax.plot([gdf.geometry.iloc[i].x, gdf.geometry.iloc[j].x],
[gdf.geometry.iloc[i].y, gdf.geometry.iloc[j].y],
'k-', alpha=0.3, linewidth=0.5)
ax.set_title('中国主要城市分布', fontsize=16, fontweight='bold')
ax.set_xlabel('经度')
ax.set_ylabel('纬度')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# 8. 最近设施查询
def find_nearest_facility(point, facilities):
"""查找最近的设施"""
min_distance = float('inf')
nearest_facility = None
for idx, facility in facilities.iterrows():
facility_point = (facility['纬度'], facility['经度'])
distance = geodesic(point, facility_point).kilometers
if distance < min_distance:
min_distance = distance
nearest_facility = facility['地址']
return nearest_facility, min_distance
# 查询离某个点最近的设施
test_point = (39.9, 116.4) # 测试点
nearest, distance = find_nearest_facility(test_point, df)
print(f"\n离{test_point}最近的设施是: {nearest}")
print(f"距离: {distance:.2f} 公里")课后练习
练习1:地理数据处理
- 创建点、线、面数据
- 进行基本空间操作
- 计算距离和面积
- 进行缓冲区分析
练习2:地图可视化
- 使用matplotlib绘制地图
- 使用folium创建交互式地图
- 创建等值线图
- 创建choropleth地图
练习3:空间分析
- 进行DBSCAN空间聚类
- 计算距离矩阵
- 进行最近邻分析
- 进行空间插值
练习4:地理编码
- 进行地理编码
- 进行反向地理编码
- 计算城市间距离
- 查找最近设施
常见问题
Q1: 如何选择坐标系?
A: 根据应用选择:
- 全球数据:WGS84
- 中国数据:CGCS2000
- 区域分析:UTM投影
Q2: 地理编码不准确怎么办?
A: 可以尝试:
- 使用更详细的地址
- 使用不同的地理编码服务
- 手动修正坐标
Q3: 如何处理大量地理数据?
A: 可以使用:
- 空间索引
- 分块处理
- 并行计算
Q4: 空间分析需要什么库?
A: 常用库包括:
- geopandas:地理数据处理
- shapely:几何操作
- folium:交互式地图
- scipy:空间统计
Q5: 如何提高地图可视化效果?
A: 可以尝试:
- 使用合适的颜色映射
- 添加图例和标签
- 调整透明度
- 使用交互式地图
下一步学习
完成今天的学习后,建议你:
- 练习处理真实的地理数据
- 掌握空间分析技术
- 学习地理编码方法
- 准备进入Day 10的学习:机器学习入门
明天我们将学习机器学习入门,这是数据分析的高级技能。