1 引言
网上关于Python第三方库Cartopy的教程不是很多,而且多少有些重复,今天笔者就提供一个Cartopy的绘图实战入门教程,希望对大家有所启发;同时,因为Cartopy绘图时会与matplotlib耦合,这里我提供了一种进阶的legend (图例) 设置方法,更为详细的操作和介绍可以参阅相关文档:
Cartopy官方文档:
https://scitools.org.uk/cartopy/docs/latest/
legend guide:
https://matplotlib.org/3.2.1/tutorials/intermediate/legend_guide.html#proxy-legend-handles
2 Cartopy
简言之就是Basemap的替代品,主要用于将各种空间数据绘制在地图上。本文绘制地图中的一些主要的空间数据(River, Roads, state等)来源于Natural Earth 数据中心。
3 实验背景&数据
这是Illinois State Will County地下水模型的一部分,之前已经通过flopy耦合modflow模拟了该地区的地下水水头变化趋势。现在引入一组实际观测数据作为校核数据,来绘制该模型区域的水头误差图error map (即计算各个坐标下实际水头和模拟水头的差值)
水头差 (error):水头模拟值存储在modflow模拟产生的hds文件中,通过flopy读取。水头实测值通过csv文件读入,计算error后存入一个DataFrame (pumping_ob)中。DataFrame中另外两个重要的参数lambx和lamby显示测量点的Lambert坐标
4 Code
1 2 3 4 5 6 7 8 9 | import matplotlib as mp import pandas as pd import pyproj # change between WGS84 and Illimap coordinates import matplotlib.pyplot as plt import numpy as np import cartopy import cartopy.crs as ccrs import cartopy.feature as cf from pykrige.uk import UniversalKriging |
pyproj库实现Lambert坐标和经纬度坐标的转换,pykrige实现克里金空间插值
需要注意,如果在Colab中运行代码,要首先下载pyproj,cartopy和pykrige:
1 2 3 | !pip install pyproj !pip install pykrige !apt-get -qq install python-cartopy python3-cartopy |
首先进行坐标转换,创建两列lat,long存储转换后的经纬度坐标。pyproj.transform 能实现不同坐标系的参数转换
1 2 3 4 | # convert position of each observed data points from (lambx,lamby) to (long,lat) for ind in pumping_ob.index: pumping_ob.loc[ind,'lat'], pumping_ob.loc[ind,'long'] = pyproj.transform(illimap,wgs84,pumping_ob.loc[ind,'lambx']*0.3048,pumping_ob.loc[ind,'lamby']*0.3048) # print(pumping_ob) |
Kriging法是空间建模时常见的插值方法,pykrige.uk.UniversalKriging 通过给定每个点的longitude,latitude,每个点的高度以及采用的方差模型来进行空间插值;
用经度的上下界 (xpoints) 和纬度的上下界 (ypoints) 分别生成模型区域网格的横纵坐标范围,以两个ndarray的形式展现;np.meshgrid根据网格点范围生成网格模型;
execute(style, xpoints, ypoints, mask=None, backend=‘vectorized’, specified_drift_arrays=None)
UK.execute返回插值结果和方差值;用NaN填充插值结果中可能出现的缺失值。
1 2 3 4 5 6 7 8 9 10 11 12 | # conduct the Universal Kriging UK = UniversalKriging(pumping_ob['long'], pumping_ob['lat'], pumping_ob['error'], variogram_model='spherical', nlags=6) # create xpoints and ypoints in space, with 0.01 spacing xpoints = np.arange(sw_long, ne_long+0.01, 0.01) ypoints = np.arange(sw_lat, ne_lat+0.01, 0.01) # create a meshgrid with xpoints and ypoints, to be used later in the code Xm, Ym = np.meshgrid(xpoints, ypoints) # calculate the interpolated grid and fill values. z, var = UK.execute('grid', xpoints, ypoints) z = z.filled(fill_value=None) |
关于以上步骤也可参阅如下英文解释:
接下来到了重头戏,我们定义一个函数error_map用来进行绘图操作。采用PlateCarree (矩形) 投影系统,然后从Natural Earth 网站获取各种空间数据,各种特征数据的定义格式如下:
cartopy.feature.NaturalEarthFeature(category, name, scale, **kwargs)
这里我引入了行政区划,河流和主要街道,再通过ax.add_feature() 将特征数据添加到图上
plt.contour 用于绘制等高线,plt.contourf 用于在等高线之间填充颜色,cmap设置配色方案,plt.clabel 用于设置各等高线处的数值标签。关于这三个的用法可以参考莫烦老师的相关视频。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | def error_map(Xm, Ym, z, df, title): """ this function uses the cartopy package to draw error map given the DataFrame """ fig = plt.figure(figsize=(16,12)) # set projection kind ax = plt.axes(projection=ccrs.PlateCarree()) # set features for different obejects states_provinces = cf.NaturalEarthFeature( category='cultural', # features are categorized as cultural or physical name='admin_1_states_provinces', # name of shapefile scale='10m', # scale of features facecolor='none') largerivers = cf.NaturalEarthFeature( category='physical', name='rivers_lake_centerlines', scale='110m', # major rivers facecolor='none') smallrivers = cf.NaturalEarthFeature( category='physical', name='rivers_lake_centerlines_scale_rank', scale='10m', # smaller rivers facecolor='none') smallestrivers = cf.NaturalEarthFeature( category='physical', name='rivers_north_america', scale='10m', # smallest rivers facecolor='none') popplaces = cf.NaturalEarthFeature( category='cultural', name='urban_areas', # plots municipal boundaries scale='10m', facecolor='plum') majorroads = cf.NaturalEarthFeature( category='cultural', name='roads', scale='10m', facecolor='none') # add defined features into axes ax.add_feature(states_provinces, edgecolor='slategray', linewidth=2.0,linestyle=':',zorder=4) ax.add_feature(popplaces,color='darkgray',alpha=0.2,linewidth=0.1, zorder=1) ax.add_feature(largerivers,edgecolor='aqua',linewidth=6.0) ax.add_feature(smallrivers,edgecolor='aqua',linewidth=6.0, zorder=2) ax.add_feature(smallestrivers,edgecolor='aqua',linewidth=6.0) ax.add_feature(majorroads,edgecolor='black',linewidth=1.0) # create contours from the interpolation and fill the interval according to given colorbar cset = plt.contour(Xm, Ym, z, np.arange(-40, 60, 10), colors='blue') C = plt.contourf(Xm, Ym, z, np.arange(-40, 60, 10), alpha=.75, cmap=plt.cm.coolwarm_r) # reverse of colorbar 'jet' plt.colorbar(C) # contour labels plt.clabel(cset, inline=1, fontsize=10, fmt='%1.0f') # show surface points on the map points = plt.scatter(df['long'],df['lat'],marker=".",color="black",label='surface points') |
这样就基本上生成了error map。但是这里有一个小问题,由于ax.add_feature() 不同于一般的plt.plot(),是无法直接通过设置label关键词和添加plt.legend()来实现图例的添加的。所以这里需要给legend添加句柄handle。主要有两种方法来实现这一效果,我这里都采用了:
1 2 3 4 5 6 7 8 9 | # add legend to the map rvr_patch = mp.patches.Patch(color='aqua',label='River') road_line = mp.lines.Line2D([],[],color='black',label='Major roads') plt.title(title,fontsize=16) plt.xlim(sw_long,ne_long) plt.ylim(sw_lat,ne_lat) plt.legend(bbox_to_anchor=(1.3,1),handles=[points,rvr_patch,road_line]) plt.show() |
mp.patches.Patch 可以通过设置填充色和边框颜色来形成一个可以传入handles中的2D patch,我这里用来设置河流图例,因为前面画河流时设置的线宽比较宽。
mp.lines.Line2D 通过传入两个空列表来创建一个没有具体方位的Line2D实例,因为前面设置道路的线宽比较小,所以我这里采样此法添加道路图例。
5 出图
出图效果还是很不错的,大部分误差值能控制在30ft的范围内,当然在北方有一小部分误差超过了40ft,这说明前期模拟效果还不是最佳,这也是该地下水模型需要继续完善校核的地方。