在地理数据可视化领域,如何合理地将连续数据划分为离散区间,直接决定了地图的表现力和准确性。一张优秀的专题地图(如人口密度图、收入分布图)背后,往往蕴含着精心设计的数据分类方案。mapclassify作为Python生态中专门用于空间数据分类的库,提供了丰富的分类算法和工具,帮助地图制作者科学、合理地表达空间数据。本文将全面解析mapclassify的使用方法及实战技巧,帮助你掌握专业的空间数据分类能力。
在制作专题地图时,我们通常需要将连续的数值数据(如人口密度、GDP、温度等)转换为有限的几个类别,并用不同的颜色或图案表示。这个过程被称为"数据分类"或"数据离散化"。
选择不恰当的分类方法可能会:
# 安装必要的库
!pip install mapclassify geopandas matplotlib contextily numpy pandas
# 导入库
import mapclassify as mc
import geopandas as gpd
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import contextily as ctx
from matplotlib.colors import LinearSegmentedColormap
mapclassify提供了多种分类算法,每种算法适用于不同的数据分布和可视化目标。
将数据范围划分为大小相等的区间。
# 生成示例数据
np.random.seed(42)
data = np.random.gamma(3, 2, 100) # 偏斜分布的数据
# 应用等间距分类
equal_interval = mc.EqualInterval(data, k=5)
print("等间距分类边界:", equal_interval.bins)
print("每个类别的数据数量:", equal_interval.counts)
# 可视化分类结果
plt.figure(figsize=(10, 6))
plt.hist(data, bins=20, alpha=0.5, color='gray')
for edge in equal_interval.bins:
plt.axvline(edge, color='red', linestyle='--')
plt.title('等间距分类法 (Equal Interval)')
plt.xlabel('数值')
plt.ylabel('频数')
plt.show()
优点:
缺点:
适用场景:
确保每个类别包含相同数量的观测值。
# 应用分位数分类
quantile = mc.Quantiles(data, k=5)
print("分位数分类边界:", quantile.bins)
print("每个类别的数据数量:", quantile.counts)
# 可视化分类结果
plt.figure(figsize=(10, 6))
plt.hist(data, bins=20, alpha=0.5, color='gray')
for edge in quantile.bins:
plt.axvline(edge, color='green', linestyle='--')
plt.title('分位数分类法 (Quantile)')
plt.xlabel('数值')
plt.ylabel('频数')
plt.show()
优点:
缺点:
适用场景:
最小化每个类内的方差,最大化类间的方差。
# 应用自然断点分类
natural_breaks = mc.NaturalBreaks(data, k=5)
print("自然断点分类边界:", natural_breaks.bins)
print("每个类别的数据数量:", natural_breaks.counts)
# 可视化分类结果
plt.figure(figsize=(10, 6))
plt.hist(data, bins=20, alpha=0.5, color='gray')
for edge in natural_breaks.bins:
plt.axvline(edge, color='blue', linestyle='--')
plt.title('自然断点分类法 (Jenks Natural Breaks)')
plt.xlabel('数值')
plt.ylabel('频数')
plt.show()
优点:
缺点:
适用场景:
基于平均值和标准差来定义类别。
# 生成正态分布数据
np.random.seed(42)
normal_data = np.random.normal(100, 15, 200)
# 应用标准差分类
std_dev = mc.StdMean(normal_data)
print("标准差分类边界:", std_dev.bins)
print("每个类别的数据数量:", std_dev.counts)
# 可视化分类结果
plt.figure(figsize=(10, 6))
plt.hist(normal_data, bins=20, alpha=0.5, color='gray')
for edge in std_dev.bins:
plt.axvline(edge, color='purple', linestyle='--')
plt.title('标准差分类法 (Standard Deviation)')
plt.xlabel('数值')
plt.ylabel('频数')
plt.show()
优点:
缺点:
适用场景:
Fisher-Jenks算法的最优实现,适合大数据集。
# Fisher-Jenks分类(优化的自然断点)
fisher_jenks = mc.FisherJenks(data, k=5)
print("Fisher-Jenks分类边界:", fisher_jenks.bins)
print("每个类别的数据数量:", fisher_jenks.counts)
# 可视化分类结果
plt.figure(figsize=(10, 6))
plt.hist(data, bins=20, alpha=0.5, color='gray')
for edge in fisher_jenks.bins:
plt.axvline(edge, color='orange', linestyle='--')
plt.title('Fisher-Jenks分类法')
plt.xlabel('数值')
plt.ylabel('频数')
plt.show()
优点:
缺点:
适用场景:
mapclassify还提供了许多其他分类方法:
# 头尾分割法(强调分布尾部)
headtail = mc.HeadTailBreaks(data)
print("头尾分割分类边界:", headtail.bins)
print("每个类别的数据数量:", headtail.counts)
# 最大间隔分类法
max_p = mc.MaximumBreaks(data, k=5)
print("最大间隔分类边界:", max_p.bins)
print("每个类别的数据数量:", max_p.counts)
# 用户自定义分类法
user_defined = mc.UserDefined(data, [5, 10, 15, 20])
print("用户自定义分类边界:", user_defined.bins)
print("每个类别的数据数量:", user_defined.counts)
不同的分类方法会产生不同的视觉效果。理解这些差异对于选择合适的分类方法至关重要。
# 创建模拟数据集
np.random.seed(42)
data = np.concatenate([
np.random.normal(50, 10, 80), # 主体数据
np.random.normal(100, 5, 20) # 少量高值
])
# 应用多种分类方法
classifiers = {
"等间距分类": mc.EqualInterval(data, k=5),
"分位数分类": mc.Quantiles(data, k=5),
"自然断点分类": mc.NaturalBreaks(data, k=5),
"Fisher-Jenks分类": mc.FisherJenks(data, k=5)
}
# 可视化比较
fig, axes = plt.subplots(len(classifiers), 1, figsize=(12, 10), sharex=True)
plt.subplots_adjust(hspace=0.5)
for i, (name, classifier) in enumerate(classifiers.items()):
axes[i].hist(data, bins=30, alpha=0.5, color='gray')
for edge in classifier.bins:
axes[i].axvline(edge, color=['red', 'green', 'blue', 'orange'][i], linestyle='--')
axes[i].set_title(f'{name} (k=5)')
axes[i].set_ylabel('频数')
# 显示每个类别的数据量
for j, count in enumerate(classifier.counts):
if j < len(classifier.counts) - 1:
x_pos = (classifier.bins[j] + classifier.bins[j+1]) / 2
axes[i].text(x_pos, max(axes[i].get_ylim()) * 0.7, f'n={count}',
ha='center', va='center', backgroundcolor='white')
axes[-1].set_xlabel('数值')
plt.tight_layout()
plt.show()
选择分类方法时,应考虑以下因素:
数据分布特征:
可视化目标:
地图用途:
受众群体:
# 加载美国县级数据
usa = gpd.read_file(gpd.datasets.get_path('usa_counties'))
# 计算人口密度
usa['pop_density'] = usa['POP2010'] / usa['ALAND'] * 1000000 # 每平方公里人口
# 筛选出本土48州数据,排除极端异常值
usa_lower48 = usa[(usa['STATEFP'] != '02') & (usa['STATEFP'] != '15')] # 排除阿拉斯加和夏威夷
usa_lower48 = usa_lower48[usa_lower48['pop_density'] < 5000] # 排除极端高值
# 应用不同分类方法
classifiers = {
"等间距分类": mc.EqualInterval(usa_lower48['pop_density'], k=5),
"分位数分类": mc.Quantiles(usa_lower48['pop_density'], k=5),
"自然断点分类": mc.NaturalBreaks(usa_lower48['pop_density'], k=5)
}
# 创建渐变色方案
colors = ['#ffffcc', '#c7e9b4', '#7fcdbb', '#41b6c4', '#1d91c0', '#225ea8']
cmap = LinearSegmentedColormap.from_list('custom_cmap', colors)
# 创建多子图比较
fig, axes = plt.subplots(1, 3, figsize=(18, 10))
plt.subplots_adjust(wspace=0.05)
for i, (name, classifier) in enumerate(classifiers.items()):
# 分类并映射到颜色
usa_lower48[f'class_{i}'] = classifier.yb
# 绘制地图
usa_lower48.plot(
column=f'class_{i}',
cmap=cmap,
ax=axes[i],
legend=True,
legend_kwds={'title': '人口密度\n(人/平方公里)', 'loc': 'lower right'}
)
axes[i].set_title(name, fontsize=14)
axes[i].set_axis_off()
axes[i].set_xlim([-125, -65])
axes[i].set_ylim([25, 50])
plt.suptitle('美国县级人口密度 - 不同分类方法比较', fontsize=18, y=0.95)
plt.tight_layout()
plt.show()
# 输出分类边界值
for name, classifier in classifiers.items():
print(f"{name}边界值: {classifier.bins.round(1).tolist()}")
# 模拟中国省级GDP数据
provinces = gpd.read_file('china_provinces.geojson') # 假设有这个文件
provinces['GDP'] = [91, 68, 44, 24, 39, 55, 77, 102, 42, 36, 27,
58, 47, 62, 38, 49, 65, 29, 41, 67, 88,
32, 72, 53, 44, 31, 48, 59, 39, 51, 47] # 示例数据
# 使用自然断点分类
natural_breaks = mc.NaturalBreaks(provinces['GDP'], k=5)
provinces['GDP_class'] = natural_breaks.yb
# 创建地图
fig, ax = plt.subplots(1, 1, figsize=(12, 10))
provinces.plot(
column='GDP_class',
cmap='OrRd',
linewidth=0.5,
ax=ax,
edgecolor='black',
legend=True,
legend_kwds={'title': 'GDP分类\n(万亿元)'}
)
# 添加省名标签
for idx, row in provinces.iterrows():
ax.annotate(row['name'], xy=(row.geometry.centroid.x, row.geometry.centroid.y),
ha='center', va='center', fontsize=8)
ax.set_title('中国省级GDP分布图 (自然断点分类)', fontsize=15)
ax.set_axis_off()
plt.tight_layout()
plt.show()
mapclassify与GeoPandas无缝集成,可以直接在GeoPandas的plot函数中使用:
# 使用mapclassify自动执行分类
usa_lower48.plot(
column='pop_density',
scheme='NaturalBreaks', # 使用自然断点分类
k=5, # 分类数量
cmap='Blues', # 色彩方案
legend=True,
figsize=(12, 8)
)
plt.title('美国县级人口密度 (自然断点分类)', fontsize=15)
plt.axis('off')
plt.show()
# 使用分位数分类绘制人口图
usa_lower48.plot(
column='pop_density',
scheme='Quantiles', # 使用分位数分类
k=5, # 分类数量
cmap='YlOrRd', # 色彩方案
legend=True,
figsize=(12, 8)
)
plt.title('美国县级人口密度 (分位数分类)', fontsize=15)
plt.axis('off')
plt.show()
# 使用自定义分类方法并添加底图
fig, ax = plt.subplots(1, 1, figsize=(12, 10))
# 选择东部地区绘制更详细的地图
eastern_states = usa_lower48[usa_lower48.geometry.centroid.x > -90]
# 应用Fisher-Jenks分类
fj = mc.FisherJenks(eastern_states['pop_density'], k=6)
eastern_states['density_class'] = fj.yb
# 绘制地图
eastern_states.to_crs(epsg=3857).plot(
column='density_class',
categorical=True,
cmap='viridis',
linewidth=0.5,
edgecolor='black',
alpha=0.7,
ax=ax
)
# 添加底图
ctx.add_basemap(ax)
# 添加标题和说明
plt.title('美国东部地区人口密度 (Fisher-Jenks分类)', fontsize=15)
plt.axis('off')
# 添加自定义图例
import matplotlib.patches as mpatches
# 获取分类边界
bounds = np.round(fj.bins, 1)
bounds = np.insert(bounds, 0, np.round(eastern_states['pop_density'].min(), 1))
# 创建图例
patches = []
for i in range(len(bounds)-1):
label = f'{bounds[i]} - {bounds[i+1]}'
color = plt.cm.viridis(i/len(bounds))
patches.append(mpatches.Patch(color=color, label=label))
plt.legend(handles=patches, title='人口密度\n(人/平方公里)',
loc='upper left', bbox_to_anchor=(1, 1))
plt.tight_layout()
plt.show()
对于高度偏斜的数据,常规分类方法可能效果不佳:
# 创建高度偏斜的数据
np.random.seed(42)
skewed_data = np.random.lognormal(0, 1, 1000)
# 原始数据的分类效果
classifiers = {
"等间距分类": mc.EqualInterval(skewed_data, k=5),
"分位数分类": mc.Quantiles(skewed_data, k=5),
"自然断点分类": mc.NaturalBreaks(skewed_data, k=5),
"头尾分割分类": mc.HeadTailBreaks(skewed_data)
}
fig, axes = plt.subplots(2, 2, figsize=(12, 8))
axes = axes.flatten()
for i, (name, classifier) in enumerate(classifiers.items()):
axes[i].hist(skewed_data, bins=50, alpha=0.5, color='gray')
for edge in classifier.bins:
axes[i].axvline(edge, color=['red', 'green', 'blue', 'purple'][i], linestyle='--')
axes[i].set_title(name)
axes[i].set_yscale('log') # 使用对数尺度以便更好地查看分布
axes[i].set_xlim([0, 20]) # 限制x轴范围以便更好地查看主体分布
plt.tight_layout()
plt.show()
# 对数变换后再分类
log_data = np.log1p(skewed_data) # log(1+x)变换
log_classifiers = {
"对数变换后等间距分类": mc.EqualInterval(log_data, k=5),
"对数变换后分位数分类": mc.Quantiles(log_data, k=5),
"对数变换后自然断点分类": mc.NaturalBreaks(log_data, k=5)
}
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
for i, (name, classifier) in enumerate(log_classifiers.items()):
axes[i].hist(log_data, bins=50, alpha=0.5, color='gray')
for edge in classifier.bins:
axes[i].axvline(edge, color=['red', 'green', 'blue'][i], linestyle='--')
axes[i].set_title(name)
plt.tight_layout()
plt.show()
# 将对数空间中的分类边界转换回原始空间
for name, classifier in log_classifiers.items():
original_bins = np.expm1(classifier.bins) # 逆变换 exp(x)-1
print(f"{name}在原始空间中的边界值: {original_bins.round(2)}")
mapclassify提供了GOF指标来评估分类方法的质量:
# 比较不同分类方法的GOF
np.random.seed(42)
test_data = np.random.gamma(2, 3, 200) # 使用gamma分布作为测试数据
methods = {
"等间距分类": mc.EqualInterval,
"分位数分类": mc.Quantiles,
"自然断点分类": mc.NaturalBreaks,
"Fisher-Jenks分类": mc.FisherJenks
}
k_values = range(3, 10) # 测试不同的k值
results = {}
for method_name, method_class in methods.items():
gof_values = []
for k in k_values:
classifier = method_class(test_data, k=k)
gof_values.append(classifier.goodness_of_fit())
results[method_name] = gof_values
# 绘制GOF比较图
plt.figure(figsize=(10, 6))
for method_name, gof_values in results.items():
plt.plot(k_values, gof_values, marker='o', label=method_name)
plt.xlabel('类别数量 (k)')
plt.ylabel('拟合优度 (GOF)')
plt.title('不同分类方法的拟合优度比较')
plt.grid(True, linestyle='--', alpha=0.7)
plt.legend()
plt.tight_layout()
plt.show()
# 使用自然断点分类并创建专业图例
data = np.random.gamma(2, 3, 100)
classifier = mc.NaturalBreaks(data, k=5)
# 模拟地图数据
np.random.seed(42)
x = np.random.rand(100)
y = np.random.rand(100)
values = data
fig, ax = plt.subplots(figsize=(10, 8))
# 为每个类别分配颜色
cmap = plt.cm.YlOrRd
boundaries = list(classifier.bins)
boundaries.insert(0, min(data))
norm = plt.Normalize(min(data), max(data))
# 绘制散点图,按类别着色
for i in range(len(boundaries)-1):
mask = (values >= boundaries[i]) & (values <= boundaries[i+1])
if i == len(boundaries)-2: # 最后一个类别包含上边界
mask = (values >= boundaries[i])
ax.scatter(x[mask], y[mask], c=[cmap(norm(np.mean([boundaries[i], boundaries[i+1]]))],
label=f'{boundaries[i]:.1f} - {boundaries[i+1]:.1f}')
# 创建图例
ax.legend(title='数值分类', fontsize=8, title_fontsize=10,
bbox_to_anchor=(1.05, 1), loc='upper left')
# 添加标题和轴标签
ax.set_title('自然断点分类示例', fontsize=15)
ax.set_xlabel('X轴')
ax.set_ylabel('Y轴')
ax.grid(True, linestyle='--', alpha=0.3)
plt.tight_layout()
plt.show()
# 根据数据特征自动选择分类方法
def auto_classifier(data, k=5):
"""根据数据特征自动选择合适的分类方法"""
# 计算数据的偏度
from scipy import stats
skewness = stats.skew(data)
# 检查是否接近正态分布
normality = stats.shapiro(data)[1] # p值
# 检查是否有极端异常值
q1, q3 = np.percentile(data, [25, 75])
iqr = q3 - q1
has_outliers = np.any((data < q1 - 1.5 * iqr) | (data > q3 + 1.5 * iqr))
# 基于数据特征选择分类方法
if normality > 0.05: # 接近正态分布
return mc.StdMean(data), "标准差分类 (正态分布)"
elif abs(skewness) > 2 or has_outliers: # 高度偏斜或有异常值
if abs(skewness) > 4: # 极端偏斜
return mc.HeadTailBreaks(data), "头尾分割分类 (极端偏斜)"
else:
return mc.Quantiles(data, k=k), "分位数分类 (偏斜分布)"
else: # 中等偏斜
return mc.NaturalBreaks(data, k=k), "自然断点分类 (默认选择)"
# 测试不同分布的数据
distributions = {
"正态分布": np.random.normal(100, 15, 200),
"均匀分布": np.random.uniform(0, 100, 200),
"偏斜分布": np.random.gamma(2, 10, 200),
"极端偏斜": np.random.lognormal(0, 1, 200),
"双峰分布": np.concatenate([np.random.normal(30, 5, 100), np.random.normal(70, 5, 100)])
}
for name, data in distributions.items():
classifier, method_name = auto_classifier(data)
print(f"{name}: 自动选择 {method_name}")
print(f" 分类边界: {classifier.bins.round(1)}")
print(f" 各类别数据量: {classifier.counts}")
print()
# 假设有城市街区数据
# 创建模拟数据
np.random.seed(42)
n = 100 # 街区数量
# 创建网格状的几何形状
from shapely.geometry import Polygon
geometries = []
for i in range(10):
for j in range(10):
# 创建网格单元作为街区
geometries.append(Polygon([(i, j), (i+1, j), (i+1, j+1), (i, j+1)]))
# 创建社会经济属性
data = {
'income': np.random.lognormal(10, 0.5, n), # 收入水平
'education': np.random.normal(15, 3, n), # 教育年限
'housing': np.random.gamma(20, 1.5, n), # 房价指数
'age': np.random.normal(40, 10, n) # 平均年龄
}
# 创建GeoDataFrame
blocks = gpd.GeoDataFrame(data, geometry=geometries)
# 对每个变量应用自然断点分类
for col in ['income', 'education', 'housing', 'age']:
classifier = mc.NaturalBreaks(blocks[col], k=5)
blocks[f'{col}_class'] = classifier.yb
# 创建多变量可视化
fig, axes = plt.subplots(2, 2, figsize=(15, 12))
axes = axes.flatten()
variables = ['income', 'education', 'housing', 'age']
cmaps = ['Reds', 'Blues', 'Greens', 'Purples']
titles = ['收入水平', '教育年限', '房价指数', '平均年龄']
for i, (var, cmap, title) in enumerate(zip(variables, cmaps, titles)):
blocks.plot(
column=f'{var}_class',
cmap=cmap,
linewidth=0.5,
edgecolor='black',
ax=axes[i],
legend=True,
legend_kwds={'title': title}
)
axes[i].set_title(title)
axes[i].set_axis_off()
plt.suptitle('城市街区社会经济特征分析', fontsize=16, y=0.98)
plt.tight_layout()
plt.subplots_adjust(top=0.9)
plt.show()
# 创建社会经济综合指数
# 标准化变量
for col in ['income', 'education', 'housing', 'age']:
blocks[f'{col}_std'] = (blocks[col] - blocks[col].mean()) / blocks[col].std()
# 创建综合指数 (示例: 收入和教育正向影响,年龄负向影响)
blocks['ses_index'] = (blocks['income_std'] * 0.4 +
blocks['education_std'] * 0.4 -
blocks['age_std'] * 0.2)
# 应用Fisher-Jenks分类
classifier = mc.FisherJenks(blocks['ses_index'], k=5)
blocks['ses_class'] = classifier.yb
# 绘制综合指数地图
fig, ax = plt.subplots(figsize=(10, 10))
blocks.plot(
column='ses_class',
cmap='RdYlBu_r', # 红(低)到蓝(高)
linewidth=0.5,
edgecolor='black',
ax=ax,
legend=True,
legend_kwds={'title': '社会经济\n综合指数'}
)
# 添加标题和注释
ax.set_title('城市街区社会经济综合指数分布', fontsize=15)
ax.set_axis_off()
# 添加说明文本
plt.figtext(0.15, 0.05, '注: 综合指数由收入(40%)、教育(40%)和年龄(-20%)加权合成',
wrap=True, horizontalalignment='left', fontsize=10)
plt.tight_layout()
plt.show()
分类方法 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
等间距分类 | 均匀分布数据;多地图比较 | 简单易懂;便于比较 | 对偏斜数据效果差 |
分位数分类 | 偏斜分布;确保每类样本量相等 | 无空类别;稳定 | 可能合并不同特征;难以比较 |
自然断点分类 | 存在自然分组;单地图展示 | 反映数据结构;视觉效果好 | 计算复杂;难以比较 |
标准差分类 | 正态分布;偏差分析 | 统计意义明确 | 不适合非正态分布 |
Fisher-Jenks | 大数据集;存在自然分组 | 优化的断点算法;效果好 | 计算开销大 |
头尾分割法 | 极端偏斜;长尾分布 | 突显高值 | 类别数不固定 |
用户自定义 | 特定阈值;政策分析 | 完全可控 | 需要领域知识 |
了解你的数据:
选择合适的分类方法:
慎重选择类别数量:
注意色彩选择:
提供清晰的图例:
考虑地图用途:
掌握了mapclassify的基础用法后,可以考虑以下进阶学习方向:
通过本文的学习,你已经掌握了使用mapclassify进行空间数据分类的核心技术。合理的数据分类不仅能提升地图的视觉效果,更能准确传达空间数据的内在规律,是地理数据分析和可视化中不可或缺的技能。希望这些知识能够帮助你创建更加专业、精确的专题地图!