本文示例代码, 数据及文件已上传至我的 GitHub 仓库 https://github.com/CNFeffery/DataScienceStudyNotes
1 简介
在上一篇文章中我们详细学习了 geoplot 中较为基础的三种绘图 API:pointplot(),polyplot()以及 webmap(), 而本文将会承接上文的内容, 对 geoplot 中较为实用的几种高级绘图 API 进行介绍.
图 1
本文是基于 geopandas 的空间数据分析系列文章的第 7 篇, 通过本文你将学习 geoplot 中的高级绘图 API.
2 geoplot 进阶
上一篇文章中的 pointplot(),polyplot 以及 webmap()帮助我们解决了在绘制散点, 基础面以及添加在线地图底图的问题, 为了制作出信息量更丰富的可视化作品, 我们需要更强的操纵矢量数据与映射值的能力, geoplot 为我们封装好了几种常见的高级可视化 API.
2.1 Choropleth
Choropleth 图又称作地区分布图或面量图, 我们在系列之前的深入浅出分层设色篇中介绍过其原理及 geopandas 实现, 可以通过将指标值映射到面数据上, 以实现对指标值地区分布的可视化.
在 geoplot 中我们可以通过 choropleth()来快速绘制地区分布图, 其主要参数如下:
df: 传入对应的 GeoDataFrame 对象
projection: 用于指定投影坐标系, 传入 geoplot.crs 中的对象
hue: 传入对应 df 中指定列名或外部序列数据, 用于映射面的颜色, 默认为 None 即不进行设色
cmap: 和 matplotlib 中的 cmap 使用方式一致, 用于控制色彩映射方案
alpha: 控制全局色彩透明度
scheme: 作用类似 geopandas 中的 scheme 参数, 用于控制分层设色, 详见本系列文章的分层设色篇, 但不同的是在 geoplot0.4.0 版本之后此参数不再搭配分层数量 k 共同使用, 而是更新为传入 mapclassify 分段结果对象, 下文中会做具体演示
legend:bool 型, 用于控制是否显示图例
legend_values:list 型, 用于自定义图例显示的各个具体数值
legend_labels:list 型, 用于自定义图例显示的各个具体数值对应的文字标签, 与 legend_values 搭配使用
legend_kwargs: 字典, 在 legend 参数设置为 True 时来传入更多微调图例属性的参数
extent: 元组型, 用于传入左下角和右上角经纬度信息来设置地图空间范围, 格式为(min_longitude, min_latitude, max_longitude, max_latitude)
figsize: 元组型, 用于控制画幅大小, 格式为(x, y)
ax:matplotlib 坐标轴对象, 如果需要在同一个坐标轴内叠加多个图层就需要用这个参数传入先前待叠加的 ax
hatch: 控制填充阴影纹路, 详情见本系列文章前作基础可视化篇图 7
edgecolor: 控制多边形轮廓颜色
linewidth: 控制多边形轮廓线型
下面我们通过实际的例子来学习 geoplot.choropleth 的用法, 这里使用到的数据为美国新型冠状肺炎各州病例数分布, 对应日期为 2020 年 5 月 14 日, 来自 GitHub 仓库: https://github.com/CSSEGISandData/COVID-19/tree/master/csse_covid_19_data/csse_covid_19_daily_reports_us; 使用到的美国本土各州矢量面数据 contiguous-usa.geojson 已上传到文章开头对应的 GitHub 仓库中:
图 2
图 3
首先我们将两张表中各自对应的州名数据作为键进行连接(注意 pd.merge 返回的结果类型为 DataFrame, 需要转换回 GeoDataFrame):
- # 按照州名列进行连接
- usa_plot_base = pd.merge(left=contiguous_usa,
- right=usa_covid19_20200513,
- left_on='state',
- right_on='Province_State')
- # 转换 DataFrame 到 GeoDataFrame, 注意要加上 crs 信息
- usa_plot_base = gpd.GeoDataFrame(usa_plot_base, crs='EPSG:4326')
接下来我们将确诊数作为映射值, 因为美国各州中纽约州和新泽西州确诊数量分别达到了 34 万和 14 万, 远远超过其他州, 所以这里作为单独的图层进行阴影填充以突出其严重程度:
- # 图层 1: 除最严重两州之外的其他州
- ax = gplt.choropleth(df=usa_plot_base.query("state not in ['New York','New Jersey']"),
- projection=gcrs.AlbersEqualArea(),
- hue='Confirmed',
- scheme=mc.FisherJenks(usa_plot_base.query("state not in ['New York','New Jersey']")['Confirmed'], k=3),
- cmap='Reds',
- alpha=0.8,
- edgecolor='lightgrey',
- linewidth=0.2,
- figsize=(8, 8)
- )
- # 图层 2: 纽约州
- ax = gplt.polyplot(df=usa_plot_base.query("state =='New York'"),
- hatch='/////',
- edgecolor='black',
- ax=ax)
- # 图层 3: 新泽西州
- ax = gplt.polyplot(df=usa_plot_base.query("state =='New Jersey'"),
- hatch='/////',
- edgecolor='red',
- extent=usa_plot_base.total_bounds,
- ax=ax)
- # 实例化 cmap 方案
- cmap = plt.get_cmap('Reds')
- # 得到 mapclassify 中 BoxPlot 的数据分层点
- bp = mc.FisherJenks(usa_plot_base.query("state not in ['New York','New Jersey']")['Confirmed'], k=3)
- bins = [0] + bp.bins.tolist()
- # 制作图例映射对象列表, 这里分配 Greys 方案到三种色彩时对应的是 [0, 0.5, 1] 这三个采样点
- LegendElement = [mpatches.Patch(facecolor=cmap(_ / 2), label=f'{int(bins[_])}-{int(bins[_+1])}')
- for _ in range(3)] + \
- [mpatches.Patch(facecolor='none',
- edgecolor='black',
- linewidth=0.2,
- hatch='/////',
- label='New York: {}'.format(usa_plot_base.query("state == \"New York\"").Confirmed.to_list()[0])),
- mpatches.Patch(facecolor='none',
- edgecolor='red',
- linewidth=0.2,
- hatch='/////',
- label='New Jersey: {}'.format(usa_plot_base.query("state == \"New Jersey\"").Confirmed.to_list()[0]))]
- # 将制作好的图例映射对象列表导入 legend()中, 并配置相关参数
- ax.legend(handles = LegendElement,
- loc='lower left',
- fontsize=8,
- title='确诊数量',
- title_fontsize=10,
- borderpad=0.6)
- # 添加标题
- plt.title('美国新冠肺炎各州病例数(截至 2020.05.14)', fontsize=18)
- # 保存图像
- plt.savefig('图 4.png', dpi=300, pad_inches=0, bbox_inches='tight')
图 4
这样我们就得到了图 4, 需要注意的是, geoplot.choropleth()只能绘制地区分布图, 传入面数据后 hue 参数必须指定对应映射列, 否则会报错, 因此这里我们叠加纽约州和新泽西州单独面图层时使用的是 polyplot().
2.2 Kdeplot
geoplot 中的 kdeplot()对应核密度图, 其基于 seaborn 中的 kdeplot(), 通过对矢量点数据分布计算核密度估计, 从而对点数据进行可视化, 可用来展示点数据的空间分布情况, 其主要参数如下:
df: 传入对应的存放点数据的 GeoDataFrame 对象
projection: 用于指定投影坐标系, 传入 geoplot.crs 中的对象
cmap: 和 matplotlib 中的 cmap 使用方式一致, 用于控制色彩映射方案
clip:GeoSeries 型, 用于为初始生成的核密度图像进行蒙版裁切, 下文会举例说明
extent: 元组型, 用于传入左下角和右上角经纬度信息来设置地图空间范围, 格式为(min_longitude, min_latitude, max_longitude, max_latitude)
figsize: 元组型, 用于控制画幅大小, 格式为(x, y)
ax:matplotlib 坐标轴对象, 如果需要在同一个坐标轴内叠加多个图层就需要用这个参数传入先前待叠加的 ax
shade:bool 型, 当设置为 False 时只有等值线被绘制出, 当设置为 True 时会绘制核密度填充
shade_lowest:bool 型, 控制是否对概率密度最低的层次进行填充, 下文会举例说明
n_levels:int 型, 控制等值线数量, 即按照概率密度对空间进行均匀划分的数量
下面我们回到上一篇文章开头的例子 -- 纽约车祸记录数据, 在其他参数均为默认的情况下, 调用 kdeplot 对车祸记录点数据的空间分布进行可视化:
- # 图层 1: 行政边界
- ax = gplt.polyplot(df=nyc_boroughs,
- projection=gcrs.AlbersEqualArea())
- # 图层 2: 默认参数下的 kdeplot
- ax = gplt.kdeplot(df=nyc_collision_factors,
- cmap='Reds',
- ax=ax)
- # 保存图像
- plt.savefig('图 5.png', dpi=300, pad_inches=0, bbox_inches='tight')
图 5
可以看到, 在 kdeplot()主要参数均为默认值的情况下, 我们得到了点数据空间分布的概率估计结果及其等高线, 譬如图中比较明显能看到的两个点分布较为密集的中心, 下面我们调整 n_levles 参数到比较大的数字:
- # 图层 1: 行政边界
- ax = gplt.polyplot(df=nyc_boroughs,
- projection=gcrs.AlbersEqualArea())
- # 图层 2:kdeplot
- ax = gplt.kdeplot(df=nyc_collision_factors,
- cmap='Reds',
- n_levels=30,
- ax=ax,
- figsize=(8, 8))
- # 保存图像
- plt.savefig('图 6.png', dpi=300, pad_inches=0, bbox_inches='tight')
图 6
可以看到在增大 n_levels 参数后, 图中等值线的数量随之增加, 下面我们设置 shade=True:
- # 图层 1: 行政边界
- ax = gplt.polyplot(df=nyc_boroughs,
- projection=gcrs.AlbersEqualArea())
- # 图层 2:kdeplot
- ax = gplt.kdeplot(df=nyc_collision_factors,
- cmap='Reds',
- shade=True,
- ax=ax,
- figsize=(8, 8))
- # 保存图像
- plt.savefig('图 7.png', dpi=300, pad_inches=0, bbox_inches='tight')
图 7
这时图像等值线间得到相应颜色的填充, 使得点分布中心看起来更加明显, 再添加参数 shade_lowest=True, 即可对空白区域进行填充:
图 8
随之而来的问题是整幅图像都被填充, 为了裁切出核密度图像的地区轮廓, 将底层行政区面数据作为 clip 的参数传入, 便得到理想的效果:
图 9
2.3 Sankey
桑基图专门用于表现不同对象之间某个指标量的流动情况, 譬如最常见的航线流向情况, 其本质是对线数据进行可视化, 并将指标值映射到线的色彩或粗细水平上, 而 geoplot 中的 sankey()可以用来绘制这种图, 尴尬的是 sankey()绘制出的 OD 流向图实在太丑, 但 sankey()中将数值映射到线数据色彩和粗细的特性可以用来进行与流量相关的可视化, 其主要参数如下:
df: 传入对应的 GeoDataFrame 对象
projection: 用于指定投影坐标系, 传入 geoplot.crs 中的对象
hue: 传入对应 df 中指定列名或外部序列数据, 用于映射线的颜色, 默认为 None 即不进行设色
cmap: 和 matplotlib 中的 cmap 使用方式一致, 用于控制色彩映射方案
alpha: 控制全局色彩透明度
scheme: 作用类似 geopandas 中的 scheme 参数, 用于控制分层设色, 详见本系列文章的分层设色篇, 但不同的是在 geoplot0.4.0 版本之后此参数不再搭配分层数量 k 共同使用, 而是更新为传入 mapclassify 分段结果对象, 下文中会做具体演示
scale: 用于设定映射线要素粗细程度的序列数据, 格式同 hue, 默认为 None 即每条线等粗
linewidth: 当不对线宽进行映射时, 该参数用于控制线宽
legend:bool 型, 用于控制是否显示图例
legend_values:list 型, 用于自定义图例显示的各个具体数值
legend_labels:list 型, 用于自定义图例显示的各个具体数值对应的文字标签, 与 legend_values 搭配使用
legend_kwargs: 字典, 在 legend 参数设置为 True 时来传入更多微调图例属性的参数
extent: 元组型, 用于传入左下角和右上角经纬度信息来设置地图空间范围, 格式为(min_longitude, min_latitude, max_longitude, max_latitude)
figsize: 元组型, 用于控制画幅大小, 格式为(x, y)
ax:matplotlib 坐标轴对象, 如果需要在同一个坐标轴内叠加多个图层就需要用这个参数传入先前待叠加的 ax
下面我们以 2015 年华盛顿街道路网日平均交通流量数据为例, 其中每个要素均为线要素, aadt 代表日均流量:
图 10
我们将其流量列映射到线的粗细程度和颜色上来, 为了美观起见我们选择系列文章分层设色篇中 palettable 的 SunsetDark 作为配色方案:
- # 选择配色方案为 SunsetDark_5
- from palettable.cartocolors.sequential import SunsetDark_5
- gplt.sankey(
- dc_roads,
- projection=gcrs.AlbersEqualArea(),
- scale='aadt',
- hue='aadt',
- limits=(0.1, 2), # 控制线宽范围
- scheme=mc.NaturalBreaks(dc_roads['aadt']),
- cmap=SunsetDark_5.mpl_colormap,
- figsize=(8, 8),
- extent=dc_roads.total_bounds
- )
- plt.savefig("图 11.png", dpi=500, pad_inches=0, bbox_inches='tight')
图 11
2.4 geoplot 中的坐标参考系
geoplot 中的坐标参考系与 geopandas 中管理起来的方式截然不同, 因为 geopandas 基于 pyproj 管理坐标参考系, 而 geoplot 中的 crs 子模块来源于 cartopy, 这一点我跟 geoplot 的主要开发者聊过, 他表示 geoplot 暂时不支持 geopandas 中那样自定义任意投影或使用 EPSG 投影, 而是内置了一系列常用的投影, 譬如我们上文中绘制美国区域时频繁使用到的 AlbersEqualArea()即之前我们在 geopandas 中通过 proj4 自定义的阿尔伯斯等面积投影, 其他常见投影譬如 Web Mercator,Robinson, 或者直接绘制球体地图, 如本文开头的图 1 就来自官方示例 (https://residentmario.github.io/geoplot/gallery/plot_los_angeles_flights.html#sphx-glr-gallery-plot-los-angeles-flights-py), 关于 geoplot 坐标参考系的细节比较简单本文不多赘述, 感兴趣的读者可以前往官网(https://residentmario.github.io/geoplot/api_reference.html#projections) 查看.
2.5 在模仿中学习
又到了最喜欢的 "复刻" 环节啦, 本文要模仿的地图可视化作品来自 https://github.com/Z3tt/30DayMapChallenge/tree/master/contributions/Day26_Hydrology, 同样是用 R 语言实现, 对全球主要河流的形态进行优雅地可视化:
图 12
针对其河流宽度方面的可视化, 我们基于上文中的 sankey()来实现, 由于原图中南极洲区域实际上是夸大了的, 其 R 源码中设置的纬度范围达到了 - 110 度, 这是原作者为了放得下标题内容, 所以在图像下部区域虚构了一篇区域, 而 geoplot 中的 extent 参数严格要求经度必须在 - 180 到 180 度之间, 纬度在 - 90 到 90 度之间. 因此在原图的基础上我们进行微调, 将标题移动到居中位置, 具体代码如下:
- from palettable.cartocolors.sequential import Teal_7_r
- import matplotlib.font_manager as fm
- from shapely.geometry import box
- # 读入世界主要河流线数据
- world_river = gpd.read_file('geometry/world_rivers_dSe.geojson')
- # 读入世界海洋面数据
- world_ocean = gpd.read_file('geometry/world_ocean.shp')
- # 图层 1: 世界范围背景色, 基于 shapely.geometry 中的 bbox 来生成矩形矢量
- ax = gplt.polyplot(df=gpd.GeoDataFrame({'geometry': [box(-180, -90, 180, 90)]}),
- facecolor='#000026',
- edgecolor='#000026')
- # 图层 2: 世界海洋面图层
- ax = gplt.polyplot(world_ocean,
- facecolor='#00003a',
- edgecolor='#00003a',
- ax=ax)
- # 图层 3: 世界主要河流线图层
- ax = gplt.sankey(world_river,
- scale='StrokeWeig',
- hue='StrokeWeig',
- scheme=mc.Quantiles(world_river['StrokeWeig'], 7),
- cmap=Teal_7_r.mpl_colormap,
- limits=(0.05, 0.4),
- figsize=(8, 8),
- extent=(-180, -90, 180, 90),
- ax=ax)
- # 添加标题
- ax.text(0, 0, 'The Rivers of the World',
- fontproperties=fm.FontProperties(fname="AlexBrush-Regular.ttf"), # 传入 Alex Brush 手写字体文件
- fontsize=28,
- color=Teal_7_r.mpl_colors[-1],
- horizontalalignment='center',
- verticalalignment='center')
- # 添加作者信息及数据来源
- ax.text(0, -15, 'Visualization by CNFeffery - Data by Natural Earth',
- fontproperties=fm.FontProperties(fname="AlexBrush-Regular.ttf"),
- fontsize=8,
- color='#599bae',
- horizontalalignment='center',
- verticalalignment='center')
- plt.savefig('图 13.png', dpi=600, pad_inches=0, bbox_inches='tight')
图 13
来源: https://www.cnblogs.com/feffery/p/12901334.html