本文由SCY翻译自《Geocomputation with R》第十三章

前提条件

  • 本章使用下列包:^[13-transport-1]

^[13-transport-1]:nabor 必须安装。

1
2
3
4
5
6
7
library(sf)
library(dplyr)
library(spDataLarge)
library(stplanr) # for processing geographic transport data
library(tmap) # map making (see Chapter 9)
library(ggplot2) # data visualization package
library(sfnetworks) # spatial network classes and functions

引言

很少有其他部门的地理空间比交通更有形。移动(克服距离)的努力是地理学“第一定律”的核心,Waldo Tobler 在1970年定义如下(Tobler 1970) :

一切事物都与其他事物相关,但是近的事物比远的事物更相关。

这个“定律”是空间自相关和其他关键地理概念的基础。它适用于各种各样的现象,如友谊网络和生态多样性,可以用交通成本来解释——在时间、精力和金钱方面——这些构成了“距离摩擦”。从这个角度来看,运输技术具有颠覆性,改变了包括移动人员和货物在内的地理实体之间的空间关系: “运输的目的是克服空间”(Rodrigue,Comtois,and Slack 2013)。

运输是一个固有的空间活动,包括从一个起点‘ A’到目的地‘ B’,通过无限的位置之间。因此,交通运输研究人员长期以来一直采用地理和计算方法来理解运动模式,以及干预措施如何能够提高他们的表现,这并不令人惊讶(Lovelace 2021)。

本章介绍了不同地理层次运输系统的地理分析:

  • Areal units: 交通模式可以通过参考区域总量来理解,比如主要的出行方式(例如,汽车、自行车或步行) ,以及生活在特定区域的人们的平均出行距离(见第13.3节)
  • Desire lines: 表示“起点-目的地”数据的直线,记录有多少人在地理空间的不同地点(点或区域)之间旅行(或可能旅行) ,这是第13.4节的主题
  • Nodes: 这些是交通系统中可以代表共同起点和目的地的点,以及公共交通站点,如公共汽车站和火车站,这是第13.5节的主题
  • Routes: 这些线路表示沿着路由网络沿着所需的线路和节点之间的路径。路由(可以表示为单行字符串或多个短段)和生成它们的路由引擎,在第13.6节中介绍
  • Route networks: 这些代表了一个区域内的道路、路径和其他线性特征的系统,在第13.6节中有介绍。
    它们可以表示为地理特征(通常是组成完整网络的短路段)或结构化为一个相互连接的图,不同段上的交通流量被运输模型师称为“流量”(Hollander 2016)。

This highlights an important feature of transport systems: they are closely linked to broader phenomena and land-use patterns.
另一个关键层次是代理,如你我以及使我们能够移动的交通工具,比如自行车和公交车。这些可以在像MATSimA/B Street这样的软件中以计算方式表示,它们使用基于代理的建模(ABM)框架,通常具有很高的空间和时间分辨率,来表示交通系统的动态性(Horni,Nagel 和 Axhausen 2016)。尽管ABM是一种具有很大整合潜力的强大的交通研究方法,尤其是与R的空间类(Thiele 2014; Lovelace and Dumont 2016) ,但它不在本章的范围内。除了地理层次和代理之外,许多交通模型中的基本分析单位是行程,即从一个起点’A’到一个终点’B’的单一目的地之旅(Hollander 2016)。行程连接了交通系统的不同层次,并可以简单地表示为连接区域质心(节点)的地理需求线,或作为遵循交通路线网络的路线。在这种情况下,代理人通常是在交通网络内移动的点实体。

交通系统是动态的(Xie and Levinson 2011)。尽管本章的重点是交通系统的地理分析,但它也提供了如何使用这种方法来模拟变化情景的见解,在第13.8节中有介绍。地理交通建模的目的可以理解为以捕捉其本质的方式简化这些时空系统的复杂性。选择适当的地理分析层次可以在不失去其最重要的特征和变量的情况下,简化这种复杂性,从而实现更好的决策和更有效的干预(Hollander 2016)。

通常,模型被设计用来解决特定问题,比如如何提高交通系统的安全性或环境性能。因此,本章围绕一个政策情境展开,将在下一节中介绍,即:如何增加布里斯托尔市的自行车使用?第14章演示了地理计算的一个相关应用:优先考虑新自行车店的位置。章节之间有一个联系:新的和有效定位的自行车基础设施可以使人们开始骑自行车,从而增加对自行车店和当地经济活动的需求。这突出了交通系统的一个重要特点:它们与更广泛的现象和土地利用模式密切相关。

布里斯托尔案例研究

本章使用的案例研究位于英格兰西部的布里斯托尔市,距离威尔士首都加的夫约30公里以东。该地区的交通网络概览在下图中展示,图中展示了多样的交通基础设施,包括自行车、公共交通和私家车。

布里斯托尔是一个充满活力和多样性的城市,拥有各种各样的交通选项。从自行车道到公交线路,再到繁忙的高速公路,布里斯托尔的交通网络反映了其居民多样化的出行需求。这为进行地理和交通研究提供了一个极为丰富的背景。


Bristol’s transport network represented by colored lines for active (green), public (railways, black) and private motor (red) modes of travel. Black border lines represent the inner city boundary (highlighted in yellow) and the larger Travel To Work Area (TTWA).

布里斯托尔是英格兰第十大的市议会,拥有50万人口,尽管其交通服务覆盖区域更广泛(参见13.3节)。该市拥有一个充满活力的经济体,包括航空航天、媒体、金融服务和旅游公司,以及两所主要大学。尽管布里斯托尔的人均收入较高,但也有严重贫困的地区(布里斯托尔市议会2015年)。

从交通角度看,布里斯托尔的铁路和公路连接非常便利,活跃出行的水平相对较高。根据Active People Survey,19%的市民每月至少骑一次自行车,88%的人每月至少走一次路(全国平均分别为15%和81%)。2011年的人口普查数据显示,8%的人口自行车上班,而全国平均仅为3%。

与许多城市一样,布里斯托尔面临严重的交通拥堵、空气质量和体力不活跃的问题。自行车出行能有效地解决这些问题:与步行相比,自行车更有可能替代汽车出行,因为其典型速度为15-20 km/h,而步行为4-6 km/h。因此,布里斯托尔的交通战略对自行车出行有着雄心勃勃的计划。

为了强调政策考虑在交通研究中的重要性,本章旨在为那些负责让人们从汽车转向更可持续出行方式(特别是步行和自行车)的人(如交通规划师、政治家和其他利益相关者)提供证据。更广泛的目标是演示地理计算如何支持基于证据的交通规划。

在本章中,你将学习如何:

  • 描述城市交通行为的地理模式
  • 识别支持多模式出行的关键公共交通节点
  • 分析旅行"愿望线",找出哪里有许多人驾车短途出行
  • 识别鼓励少开车、多骑自行车的自行车路线位置

为了在实践方面展开本章的内容,下一节将开始加载关于旅行模式的区域数据。这些区域级别的数据集虽小,但对于获取对城市整体交通系统的基本了解至关重要。

交通区域

尽管交通系统主要基于线性特征和节点——包括路径和车站——从面数据开始通常更有意义,以将连续空间划分为可触及的单位 (Hollander 2016)。除了定义研究区域的边界(在这种情况下是布里斯托尔)之外,对交通研究人员尤为感兴趣的两种区域类型是:起源区域和目的区域。通常,相同的地理单位用于起点和目的地。然而,不同的分区系统,例如 ‘工作区’,可能更适合代表在有许多’旅行吸引点’的区域内旅行目的地的增加密度 (国家统计局2014年)。

最简单定义研究区域的方式通常是第一个由OpenStreetMap返回的匹配边界。这可以通过诸如 osmdata::getbb("Bristol", format_out = "sf_polygon", limit = 1) 的命令来完成。这返回一个 sf 对象(或者如果没有指定 limit = 1,则返回一组 sf 对象),代表最大匹配城市区域的范围,要么是边界框的矩形多边形,要么是一个详细的多边形边界。[^13-transport-2]对于布里斯托尔,返回了一个详细的多边形,由 spDataLarge 包中的 bristol_region 对象表示。请参见上图中的内部蓝色边界:这种方法有几个问题:

[^13-transport-2]: 在第一个匹配没有提供正确名称的情况下,应指定国家或地区,例如位于美国的布里斯托尔应为 Bristol Tennessee

  • 第一个由OSM返回的OSM边界可能不是当地政府使用的官方边界
  • 即使OSM返回官方边界,这也可能不适合交通研究,因为它们与人们的旅行路线关系不大

工作通勤区域(TTWAs)通过创建一个类似于水文流域的分区系统来解决这些问题。TTWAs最初被定义为连续区域,在其中75%的人口前往工作(Coombes, Green, and Openshaw 1986),这是本章使用的定义。由于布里斯托尔是一个主要的雇主,吸引了周边城镇的旅行,因此其TTWA比城市界限要大得多(见上图)。代表这一以交通为导向的边界的多边形存储在由章节开始时加载的 spDataLarge 包提供的 bristol_ttwa 对象中。

本章使用的起点和终点区域是相同的:官方定义的中等地理分辨率区域(它们的官方名称是中层超级输出区域或MSOAs)。
每个区域大约有8,000人。这种行政区域可以为交通分析提供重要的背景信息,例如可能最受特定干预措施影响的人群类型(例如, Moreno-Monroy, Lovelace, and Ramos 2017)。

这些区域的地理分辨率很重要:小区域具有高地理分辨率通常更可取,但它们在大区域中的高数量可能对处理(尤其是对于起点-终点分析,在其中可能性作为区域数量的非线性函数增加)有影响 (Hollander 2016)。

📌与小区域相关的另一个问题与匿名规则有关。为了使在区域内无法推断个人身份,详细社会人口统计变量通常仅在低地理分辨率下可用。例如,旅行方式的年龄和性别细分在英国的地方当局层面上可用,但不在更高的输出区域层面上,其中每个区域大约包含100户家庭。有关更多详细信息,请参阅 www.ons.gov.uk/methodology/geography。

本章中使用的102个区域存储在 bristol_zones 中,如下图所示。注意,在人口密集地区,区域变得更小:每个区域都有类似数量的人。bristol_zones 不包含关于交通的属性数据,只有每个区域的名称和代码:

1
2
names(bristol_zones)
#> [1] "geo_code" "name" "geometry"

为了添加旅行数据,我们将执行属性连接,这是在矢量属性连接节中描述的常见任务。我们将使用来自英国2011年人口普查关于工作通勤的问题的旅行数据,该数据存储在 bristol_od 中,由 ons.gov.uk 数据门户提供。bristol_od 是一个关于英国2011年人口普查中各区域间通勤旅行的起点-终点(OD)数据集。第一列是起始区域的ID,第二列是目的区域。bristol_od 的行数比 bristol_zones 多,代表的是区域之间的旅行,而不是区域本身:

1
2
3
4
nrow(bristol_od)
#> [1] 2910
nrow(bristol_zones)
#> [1] 102

前一个代码块的结果显示,每个区域有超过10个起点-终点(OD)对,这意味着在将其与 bristol_zones 进行连接之前,我们需要对起点-终点数据进行聚合,如下图所示

1
2
3
4
zones_attr = bristol_od |> 
group_by(o) |>
summarize(across(where(is.numeric), sum)) |>
dplyr::rename(geo_code = o)

前面的代码块执行了以下操作:

  • 按照起点区域(包含在o列中)对数据进行了分组
  • 如果bristol_od 数据集中的变量是数值型的,就对它们进行了聚合,以找出每个区域中按交通方式分布的人口总数[^13-transport-3]
  • 将分组变量o重命名,使其与bristol_zones对象中的ID列geo_code匹配

[^13-transport-3]: _if 后缀要求对变量提出一个 TRUE/FALSE 的问题,在这个情况下是’是否为数值型?’ 只有返回 true 的变量会被汇总。

由此产生的对象zones_attr是一个数据框,其行代表各个区域和一个ID变量。我们可以使用%in%操作符验证这些ID是否与zones数据集中的ID匹配,操作如下:

1
2
3
summary(zones_attr$geo_code %in% bristol_zones$geo_code)
#> Mode TRUE
#> logical 102

结果显示,新对象中包含所有102个区域,而zone_attr具有可以与区域连接的形式[^13-transport-4]。这是通过使用连接函数 left_join() 来完成的(注意,在这里inner_join()也会产生相同的结果)。

[^13-transport-4]: 在实际数据中,检查ID在反方向上是否匹配也同样重要。这可以通过改变summary()命令中ID的顺序来完成——summary(bristol_zones$geo_code %in% zones_attr$geo_code)——或者通过如下使用setdiff()setdiff(bristol_zones$geo_code, zones_attr$geo_code)

1
2
3
4
5
6
zones_joined = left_join(bristol_zones, zones_attr, by = "geo_code")
sum(zones_joined$all)
#> [1] 238805
names(zones_joined)
#> [1] "geo_code" "name" "all" "bicycle" "foot"
#> [6] "car_driver" "train" "geometry"

结果是zones_joined,它包含新的列,代表研究区域内每个区域起始的总出行次数(接近四分之一百万次)以及它们的出行方式(自行车、步行、汽车和火车)。出行起点的地理分布在下图的左侧地图中有所体现。这显示了大多数区域在研究区内有0至4000次的出行起始。生活在布里斯托尔市中心附近的人进行的出行次数更多,而在城市边缘的则较少。这是为什么呢?记住我们只在研究区域内处理出行:边缘区域低出行次数可以归因于这些周边区域的许多人会前往研究区域之外的其他地区。通过一个特殊的目的地ID,研究区域之外的出行可以被包括在区域模型中,该ID涵盖了任何前往模型中未表示的区域的出行(Hollander 2016)。然而,bristol_od中的数据简单地忽略了这样的出行:它是一个“区域内”的模型。

与OD(出发-目的地)数据集可以聚合到出发区域的方式相同,它们也可以被聚合以提供关于目的地区域的信息。人们倾向于聚集在中心地区。这解释了为什么下图右侧面板中表示的空间分布相对不均匀,最常见的目的地区域集中在布里斯托尔市中心。结果是zones_od,其中包含一个新列,报告了任何出行方式的目的地数量,如下所创建:

1
2
3
4
5
zones_destinations = bristol_od |> 
group_by(d) |>
summarize(across(where(is.numeric), sum)) |>
select(geo_code = d, all_dest = all)
zones_od = inner_join(zones_joined, zones_destinations, by = "geo_code")

下面的代码创建了一个简化版本的下图(参见书籍GitHub仓库的 code 文件夹中的 12-zones.R 文件以重现该图,以及有关使用tmap创建分面地图的详细信息):

1
2
qtm(zones_od, c("all", "all_dest")) +
tm_layout(panel.labels = c("Origin", "Destination"))


Number of trips (commuters) living and working in the region. The left map shows zone of origin of commute trips; the right map shows zone of destination (generated by the script 13-zones.R).

欲望线

欲望线连接起点和终点,表示人们想要去的地方,通常是在各个区域之间。它们代表了从A点到B点最快的“直线”或“鸟飞”路线,如果没有建筑物和弯曲道路的阻碍的话(我们将在下一节看到如何将欲望线转化为实际路线)。通常,欲望线在地理上表示为每个区域的地理(或人口加权)中心的起点和终点。这是我们将在本节中创建和使用的欲望线类型,尽管值得注意的是,“抖动”技术能够增加多个起点和终点,以提高基于OD数据构建的分析的空间覆盖和准确性 (Lovelace,Félix 和 Carlino 2022)。

我们已经在数据集bristol_od中加载了代表欲望线的数据。这个起点-终点(OD)数据框对象表示在od所代表的区域之间旅行的人数,如下表所示。要按所有旅行对OD数据进行排序,然后只过滤出前5名,请输入:

1
2
od_top5 = bristol_od |> 
slice_max(all, n = 5)

Table: Sample of the top 5 origin-destination pairs in the Bristol OD data frame, representing travel desire lines between zones in the study area.

o d all bicycle foot car_driver train
E02003043 E02003043 1493 66 1296 64 8
E02003047 E02003043 1300 287 751 148 8
E02003031 E02003043 1221 305 600 176 7
E02003037 E02003043 1186 88 908 110 3
E02003034 E02003043 1177 281 711 100 7

生成的表格提供了关于布里斯托尔通勤(上班往返)出行模式的快照。它表明,在前5大起点-终点(OD)组合中,步行是最受欢迎的出行方式,而E02003043 区域是一个受欢迎的目的地(布里斯托尔市中心,所有前5大OD组合的目的地),并且区内 出行,从E02003043 区域的一个部分到另一个部分(表的第一行),是数据集中出行最多的OD组合。但从政策角度看,表中呈现的原始数据用处有限:除了它仅包含了2,910个OD组合中的一小部分之外,它几乎没有告诉我们在哪里需要政策措施,或者有多少比例的出行是通过步行和骑自行车完成的。
以下命令计算了由这些积极模式完成的每条欲望线的百分比:

1
2
bristol_od$Active = (bristol_od$bicycle + bristol_od$foot) /
bristol_od$all * 100

有两种主要类型的OD(起点-终点)组合:区间(Interzonal)区内(Intrazonal)。区间的OD组合代表了目的地与起点不同区域之间的出行。区内的OD组合代表了在同一区域内的出行(参见表的顶行)。以下代码块将 od_bristol 分为这两种类型:

1
2
od_intra = filter(bristol_od, o == d)
od_inter = filter(bristol_od, o != d)

下一步是将区间(interzonal)的OD组合转换为一个表示“欲望线(desire lines)”的sf对象,这可以通过使用 stplanr 函数 od2line() 来在地图上进行绘制。[^13-transport-5]

[^13-transport-5]: od2line() 的工作方式是通过匹配 bristol_od 对象的前两列中的ID与地理 zones_od 对象中的 zone_code ID列。需要注意的是,该操作会发出一个警告,因为 od2line() 是通过将每一对起点-终点分配给其起源和目的地区域的质心\index{centroid}来工作的。对于实际应用,人们会使用由投影数据生成的质心值,或者更好地使用基于人口加权的质心[@lovelace_propensity_2017]。

1
2
desire_lines = od2line(od_inter, zones_od)
#> Creating centroids representing desire line start and end points.

下图展示了结果的示例,其简化版本可以通过以下命令创建(要完全复制该图形,请查看 13-desire.R 中的代码,有关使用 tmap 进行可视化的详细信息,请参见章节高级制图):

1
qtm(desire_lines, lines.lwd = "all")


Desire lines representing trip patterns in Bristol, with width representing number of trips and color representing the percentage of trips made by active modes (walking and cycling). The four black lines represent the interzonal OD pairs in Table 7.1.

该地图显示,市中心主导了该地区的交通模式,这暗示政策应当优先考虑这里,尽管也可以看到一些边缘的次中心。欲望线是交通系统中重要的概括性组成部分。更具体的组成部分包括节点,这些节点有特定的目的地(而不是欲望线中所表示的假设直线)。下一节将涵盖节点。

节点

在地理交通数据集中,节点是组成交通网络的主要线性特征之间的点。大致上,有两种主要类型的交通节点:

  1. 不直接位于网络上的节点,例如区域质心或个体的起点和终点(如住宅和工作场所)。
  2. 是交通网络一部分的节点。
    从技术上讲,一个节点可以位于交通网络上的任何点,但实际上它们通常是特殊类型的顶点,例如路径(交叉口)之间的交点和进入或退出交通网络的点,如公交站和火车站[^13-transport-6]。

交通网络可以表示为图形,其中每个段通过地理线(代表边)与网络中的一个或多个其他边相连。可以通过“质心连接器”添加网络之外的节点,这些新的路线段连接到网络上附近的节点(Hollander 2016)[^13-transport-7]。网络中的每个节点然后通过一个或多个代表网络上个别段的“边”进行连接。我们将在路线网络节中看到如何将交通网络表示为图。

公共交通站点是特别重要的节点,可以表示为两种类型的节点:是道路的一部分的公交站,或由距离铁路轨道数百米的行人入口点代表的大型铁路站。我们将使用火车站来说明与布里斯托尔市增加自行车使用相关的公共交通节点。这些站由 spDataLargebristol_stations 中提供。

一个常见的障碍是,从家到工作的距离太远,无法步行或骑自行车。公共交通可以通过为进入城市的常见路线提供快速和高容量的选择来减少这一障碍。从积极出行的角度来看,长途旅行的公共交通“腿”将旅行分为三部分:

  • 起点腿,通常从住宅区到公共交通站
  • 公共交通腿,通常从离旅行起点最近的站到离目的地最近的站
  • 目的地腿,从下车站到目的地

欲望线节进行的分析的基础上,公共交通节点可用于为可乘坐公共汽车和(在本例中使用的)火车的旅行构建三部分欲望线。第一阶段是确定具有最多公共交通出行的欲望线,在我们的情况下这很容易,因为我们之前创建的数据集 desire_lines 已经包含了描述火车旅行数量的变量(也可以使用如 OpenTripPlanner 等公共交通路线服务来估计公共交通潜力)。为了使该方法更容易遵循,我们将仅选择关于铁路使用方面的前三条欲望线。

1
desire_rail = top_n(desire_lines, n = 3, wt = train)

现在的挑战是将每一条这样的线分解为三部分,代表通过公共交通节点的出行。这可以通过将一条欲望线转换为一个由三个线几何体组成的多线字符串对象来实现,这三个线几何体分别代表行程的起点、公共交通和终点阶段。这个操作可以分为三个阶段:矩阵创建(代表起点、终点和表示铁路站的“途经”点)、最近邻标识和转换为多线字符串。这些都由line_via()函数来完成。这个stplanr函数接受输入线和点,并返回一份欲望线的副本——具体细节请参见?line_via()
输出与输入线相同,只不过它有新的几何列,代表通过公共交通节点的行程,如下所示:

1
2
3
4
5
ncol(desire_rail)
#> [1] 9
desire_rail = line_via(desire_rail, bristol_stations)
ncol(desire_rail)
#> [1] 12

如下图所示,初始的 desire_rail 线现在有三个额外的几何列表列,分别代表从家到起点站、从那里到目的地,以及最后从目的地站到目的地的旅行。在这种情况下,目的地段非常短(步行距离),但起点段可能足够远,以证明需要在骑行基础设施上进行投资,以鼓励人们在上班途中到居住区周围的三个起点站骑自行车,如下图所示。


Station nodes (red dots) used as intermediary points that convert straight desire lines with high rail usage (thin green lines) into three legs: to the origin station (orange) via public transport (blue) and to the destination (pink, not visible because it is so short).

路线

从地理角度看,路线是不再直线的需求线:起点和终点与旅行的需求线表示相同,但从A到B的路径更为复杂。路线的几何形状通常(但不总是)由交通网络决定。

虽然需求线仅包含两个顶点(其起点和终点),但路线可以包含任意数量的顶点,代表由直线连接的A和B之间的点:这就是linestring几何形状的定义。覆盖大距离或遵循复杂网络的路线可能有数百个顶点;基于网格或简化道路网络的路线通常较少。

路线是由需求线生成的,或更常见地,由包含代表需求线的坐标对的矩阵生成。这个路由过程是由一系列广义定义的路由引擎完成的:软件和Web服务,它们返回描述如何从起点到终点的几何形状和属性。根据它们相对于R运行的位置,路由引擎可以被分类为:

  • 内存路由,使用使路线计算成为可能的R包
  • 本地托管的、外部于R的路由引擎,可以从R中调用
  • 由外部实体远程托管的路由引擎,提供一个可以从R中调用的Web API

在描述每一个之前,值得概述分类路由引擎的其他方式。路由引擎可以是多模式的,意味着它们可以计算由多种交通方式组成的行程,或者不是。多模式路由引擎可以返回由不同交通方式组成的多个的结果。从住宅区到商业区的最佳路线可能涉及1)步行到最近的公交车站,2)乘公交车到离目的地最近的节点,以及3)步行到目的地,给定一组输入参数。这三段之间的转换点通常被称为“进入”和“出口”,意味着上下公共交通工具。像R5这样的多模式路由引擎比如OSRM这样的’单一模式’路由引擎更为复杂,并且有更大的输入数据要求。

多模式引擎的一个主要优势是它们能够表示由火车、公共汽车等组成的“公共交通”行程。多模型路由引擎需要代表公共交通网络的输入数据集,通常以General Transit Feed Specification(GTFS)文件形式,这些可以用tidytransitgtfstools包(也有其他用于处理GTFS文件的包和工具)中的函数进行处理。对于专注于特定(非公共)交通方式的项目,单一模式路由引擎可能就足够了。另一种分类路由引擎(或设置)的方式是通过输出的地理级别:路线、段和环节。

路线、段和环节

路由引擎可以在三个地理级别生成输出:路线、段和环节:

  • 路线级别的输出每个起点-终点对包含一个单一特征(通常是数据框表示中的多线几何和关联行),意味着每次行程的数据有一行。
  • 级别的输出在每个起点-终点对内的每个模式都包含一个单一特征和相关属性。对于仅涉及一种模式的行程(例如,从家到工作的驾驶,忽略走到汽车的短距离),段与路线相同:即汽车行程。对于涉及公共交通的行程,段提供关键信息。r5r函数detailed_itineraries()返回的段有时令人困惑地被称为“环节”。
  • 环节级别的输出提供了有关路线的最详细信息,具有交通网络的每个小段的记录。通常,环节在长度上类似于,或与,OpenStreetMap中的道路相同。cyclestreets函数journey()返回在环节级别的数据,这些数据可以通过对stplanrroute()函数返回的起点和终点级别数据进行分组而聚合。

大多数路由引擎默认返回路线级别的输出,尽管多模式引擎通常在段级别提供输出(每个连续运动的单一交通模式一个特征)。 环节级别的输出有提供更多细节的优点。 cyclestreets包返回每条路线的多个“安静度”级别,使得可以识别自行车网络中的“最薄弱环节”。 环节级别输出的劣势包括增加的文件大小和与额外细节相关的复杂性。

使用函数stplanr::overline()[@morgan_travel_2020],路线级别的结果可以转换为环节级别的结果。在使用环节或段级别的数据时,通过对表示行程起点和终点的列进行分组,并汇总/聚合包含环节级别数据的列,可以返回路线级别的统计数据。

使用R进行内存内路由

R中的路由引擎允许将存储为R对象的内存内路由网络用作路由计算的基础。选项包括sfnetworksdodgrcppRouting 包,每个包都提供了它们自己的类系统来表示路由网络,这是下一节的主题。

尽管快速和灵活,使用原生R的路由选项通常比用于现实路由计算的专用路由引擎更难设置。路由是一个复杂问题,开源路由引擎已经投入了很多时间,这些引擎可以下载并在本地主机上运行。另一方面,基于R的路由引擎可能非常适用于模型实验和网络上变化影响的统计分析。在单一语言中更改路由网络特性(或与不同路由段类型相关联的权重)、重新计算路由和分析多个场景下的结果对研究应用有益。

本地托管的专用路由引擎

本地托管的路由引擎包括OpenTripPlanner、Valhalla 和R5(它们是多模式的),以及OpenStreetMap路由机器(OSRM)(它是“单模式”的)。这些可以通过opentripplannervalhallar5rosrm 包从R中访问(Morgan 等,2019; Pereira 等,2021)。本地托管的路由引擎在用户的计算机上运行,但在一个与R分开的进程中。它们的优点包括执行速度和对不同运输模式的权重配置文件的控制。劣势包括在本地表示复杂网络的困难;时间动态(主要是由于交通);以及需要专用外部软件。

远程托管的专用路由引擎

远程托管的路由引擎使用web API发送有关起点和终点的查询并返回结果。基于开源路由引擎的路由服务,例如OSRM的公开可用服务,在从R调用时与本地托管实例的工作方式相同,只需更新指定“基础URL”的参数即可。然而,由于外部路由服务托管在专用机器上(通常由有激励生成准确路由的商业公司资助),这可能给它们带来优势,包括:

  • 提供全球(或通常至少在大区域内)的路由服务
  • 已建立的路由服务通常会定期更新,并且经常能够响应交通水平
  • 路由服务通常在专用硬件和软件上运行,包括像负载均衡器这样的系统以确保性能稳定

远程路由服务的劣势包括批量作业不可能时的速度(它们通常依赖于一条条路由的基础上通过互联网进行数据传输)、价格(例如,谷歌路由API限制了免费查询的数量)和许可问题。

googlewaymapbox 包通过提供对Google和Mapbox的路由服务的访问来展示这种方法。免费(但有速率限制)的路由服务包括 OSRMopenrouteservice.org,它们可以通过 osrmopenrouteservice 包从R中访问,后者不在CRAN上。还有更具体的路由服务,例如由 CycleStreets.net 提供的,这是一个自行车旅行规划器和非营利性交通技术公司“为骑自行车者,由骑自行车者”。虽然R用户可以通过 cyclestreets 包访问CycleStreets路由,但许多路由服务缺乏R接口,这代表了一个巨大的软件包开发机会:构建一个R包以提供一个接口到web API可能是一种有益的经历。

R包用于计算和导入代表交通网络上路由的数据的广泛范围是一种优势,这意味着该语言近年来越来越多地用于交通研究。然而,这种包和方法的激增的一个小缺点是有很多包和函数名称需要记住。
stplanr 包通过提供一个用于生成路由的统一接口 route() 函数来解决这个问题。该函数接受广泛的输入,包括地理欲望线(使用 l = 参数)、坐标甚至表示独特地址的文本字符串,并返回作为一致 sf 对象的路由数据。

路径规划:一个实际示例

与其对部分生成的所有期望线进行路径规划,我们专注于高度政策相关的一个子集。在尝试处理整个数据集之前,先对一个子集进行计算密集型操作通常是明智的,这同样适用于路径规划。路径规划可能需要消耗大量时间和内存,并生成大型对象,这是因为路线对象的详细几何形状和额外属性。因此,我们将在本节中过滤期望线,然后计算路径。

当骑行替代开车出行时,效益最大。短距离(大约5公里,以20公里/小时的速度可以在15分钟内骑完)有相对较高的被骑行的概率,当使用电动自行车进行出行时,最大距离会增加(Lovelace 等,2017)。以下代码片段考虑了这些因素,过滤了期望线,并返回了表示OD对(起点到终点对)的对象 desire_lines_short,在这些OD对之间有很多(100+)短距离(2.5至5公里的欧几里得距离)的车程:

1
2
3
desire_lines$distance_km = as.numeric(st_length(desire_lines)) / 1000
desire_lines_short = desire_lines |>
filter(car_driver >= 100, distance_km <= 5, distance_km >= 2.5)

在上面的代码中,st_length() 计算了每个期望线的长度。dplyrfilter() 函数基于上述标准过滤了 desire_lines 数据集。下一步是将这些期望线转换为路线。这是通过下面的代码块中使用公开可用的OSRM服务和 stplanrroute()route_osrm() 函数来完成的:

1
2
routes_short = route(l = desire_lines_short, route_fun = route_osrm,
osrm.profile = "bike")

输出是 routes_short,一个代表适用于骑自行车的交通网络上的路线的 sf 对象(至少根据 OSRM 路由引擎是这样),每个期望线对应一个。注意:像上面的命令中那样调用外部路由引擎只有在有互联网连接的情况下才能工作(有时还需要存储在环境变量中的 API 密钥,尽管在这种情况下不需要)。除了 desire_lines 对象中包含的列之外,新的路线数据集还包含 distance(这次是指路线距离)和 duration(以秒为单位)列,这些提供了有关每条路线性质的潜在有用额外信息。我们将绘制沿着这些线路进行许多短途汽车行驶的期望线和骑行路线。通过使路线的宽度与可能被替换的汽车行驶数量成比例,提供了一种有效的方式来优先考虑对道路网络进行干预(Lovelace 等 2017)。
下面的代码块绘制了期望线和路线,该图显示了人们驾驶短距离的路线:^13-transport-8


Routes along which many (100+) short (<5km Euclidean distance) car journeys are made (red) overlaying desire lines representing the same trips (black) and zone centroids (dots).

通过在交互式地图上绘制结果,例如使用 mapview::mapview(st_geometry(routes_short)),可以看到许多短途汽车行程发生在布拉德利斯托克(Bradley Stoke)及其周围,距离布里斯托尔(Bristol)市中心大约10公里以北。找出该地区高度依赖汽车的原因很容易:根据维基百科,布拉德利斯托克是“欧洲最大的由私人投资建设的新城镇”,这暗示了有限的公共交通供应。此外,该城镇被包括M4和M5高速公路在内的大型(对自行车不友好)的道路结构所环绕(tallon 2007)。

将旅行需求线转换为路线有很多好处。重要的是要记住,我们不能确定有多少(如果有的话)行程会沿着路由引擎计算出的确切路线进行。然而,路线和街道/道路/段级别的结果可能具有很高的政策相关性。路段结果可以根据可用数据(Lovelace 等 2017),实现在最需要的地方优先进行投资。

路线网络

虽然路线通常包含有关旅行行为的数据,在与需求线和OD(起点-终点)对相同的地理层级上,路线网络数据集通常代表物理交通网络。路线网络中的每个大致对应于交叉口之间的连续街道段,并且只出现一次,尽管段的平均长度取决于数据源(本节中使用的由OSM(开放街道地图)派生的bristol_ways数据集中的段平均长度刚好超过200米,标准差接近500米)。段长度的可变性可以由这样一个事实来解释:在一些农村地区,交叉口相距很远,而在密集的城市区域,每隔几米就有交叉口和其他段的断点。

路线网络可以是交通数据分析项目的输入或输出,或者两者都是。任何涉及路线计算的交通研究都需要内部或外部路由引擎中的路线网络数据集(在后一种情况下,路线网络数据不一定需要导入到R中)。然而,路线网络在许多交通研究项目中也是重要的输出:总结诸如特定段上可能进行的潜在行程数量的数据,并以路线网络的形式表示,可以帮助在最需要的地方优先进行投资。

为了演示如何从路线级别的数据中创建路线网络作为输出,想象一个简单的模式转移场景。假设0到3公里的路线距离之间的50%的汽车行程被自行车取代,这一百分比每增加1公里的路线距离就减少10个百分点,以至于6公里的汽车行程有20%被自行车取代,而8公里或更长的汽车行程没有被自行车取代。当然,这是一个不现实的情景(Lovelace 等 2017),但它是一个有用的起点。
在这种情况下,我们可以按照以下方式模拟从汽车到自行车的模式转移:

1
2
3
4
5
6
7
8
9
10
11
12
13
uptake = function(x) {
case_when(
x <= 3 ~ 0.5,
x >= 8 ~ 0,
TRUE ~ (8 - x) / (8 - 3) * 0.5
)
}
routes_short_scenario = routes_short |>
mutate(uptake = uptake(distance / 1000)) |>
mutate(bicycle = bicycle + car_driver * uptake,
car_driver = car_driver * (1 - uptake))
sum(routes_short_scenario$bicycle) - sum(routes_short$bicycle)
#> [1] 3733

在创建了一个大约有4000次行程从驾驶转向骑自行车的场景后,我们现在可以模拟这个更新后的模拟自行车活动将在哪里发生。为此,我们将使用stplanr包中的overline()函数。该函数在交叉口(两条或更多的折线几何相交的地方)处断开折线,并计算每个唯一路段的聚合统计信息(Morgan 和 Lovelace 2020),它将包含路线的对象和要汇总的属性的名称作为第一和第二个参数:

1
route_network_scenario = overline(routes_short_scenario, attrib = "bicycle")

前面两个代码块的输出总结如下面的图所示。


Illustration of the percentage of car trips switching to cycling as a function of distance (left) and route network level results of this function (right).

具有在路段级别的记录的交通网络,通常带有诸如道路类型和宽度之类的属性,构成了一种常见的路线网络。这种路线网络数据集在全球范围内都可以从OpenStreetMap获得,并可以使用诸如osmdataosmextract这样的软件包进行下载。为了节省下载和准备OSM的时间,我们将使用来自spDataLarge包的bristol_ways对象,这是一个带有LINESTRING几何图形和属性的sf对象,代表了案例研究区域内交通网络的一个样本(详见?bristol_ways以获取详细信息),如下面的输出所示:

1
2
3
4
5
summary(bristol_ways)
#> highway maxspeed ref geometry
#> cycleway:1721 Length:6160 Length:6160 LINESTRING :6160
#> rail :1017 Class :character Class :character epsg:4326 : 0
#> road :3422 Mode :character Mode :character +proj=long...: 0

输出显示bristol_ways代表了交通网络上刚刚超过6千个路段。这种和其他地理网络可以表示为数学图,网络上的节点由边连接。一些R软件包已经为处理这样的图而开发,尤其是igraph。您可以手动将路线网络转换为igraph对象,但地理属性将会丢失。为了克服igraph的这一局限性,开发了sfnetworks软件包,该软件包能够同时将路线网络表示为图地理线。我们将在bristol_ways对象上演示sfnetworks的功能。

1
2
3
4
bristol_ways$lengths = st_length(bristol_ways)
ways_sfn = as_sfnetwork(bristol_ways)
class(ways_sfn)
#> [1] "sfnetwork" "tbl_graph" "igraph"
1
2
3
4
5
6
7
8
9
ways_sfn
#> # A sfnetwork with 5728 nodes and 4915 edges
#> # A directed multigraph with 1013 components with spatially explicit edges
#> # Node Data: 5,728 × 1 (active)
#> # Edge Data: 4,915 × 7
#> from to highway maxspeed ref geometry lengths
#> <int> <int> <fct> <fct> <fct> <LINESTRING [°]> [m]
#> 1 1 2 road <NA> B3130 (-2.61 51.4, -2.61 51.4, -2.61 51.… 218.
#> # …

由于空间考虑,前一个代码块的输出(最后的输出被缩短,仅包含最重要的8行)显示ways_sfn是一个复合对象,以图和空间形式包含节点和边。ways_sfnsfnetwork类,该类基于igraph包中的igraph类。在下面的示例中,计算了’边介数’,也就是经过每条边的最短路径的数量(有关更多详细信息,请参见?igraph::betweenness)。边介数计算的输出显示在下图,该图以用overline()函数计算的自行车路线网络数据集作为叠加层进行比较。结果表明,每个图的边代表一个路段:靠近道路网络中心的路段具有最高的介数值,而靠近布里斯托尔中心的路段基于这些简单数据集具有更高的自行车潜力。

1
2
3
ways_centrality = ways_sfn |> 
activate("edges") |>
mutate(betweenness = tidygraph::centrality_edge_betweenness(lengths))


Illustration of route network datasets. The grey lines represent a simplified road network, with segment thickness proportional to betweenness. The green lines represent potential cycling flows (one way) calculated with the code above

人们还可以使用sfnetworks包中的这种路由网络的图表示法来找到起点和终点之间的最短路线。与可能的情况相比,本节介绍的方法相对简单。sfnetworks开放的双重图/空间功能使得许多新的强大技术得以实现,这些在本节中无法完全涵盖。然而,本节确实为进一步探索和研究该领域提供了一个坚实的起点。最后一点是,我们上面使用的示例数据集相对较小。也可能值得考虑如何将工作适应到更大的网络:在数据的子集上测试方法,并确保您有足够的RAM将有所帮助,尽管也值得探索其他优化用于大型网络的交通网络分析工具,比如R5 (Alessandretti 等 2022)。

优先考虑新基础设施

本节演示了地理计算如何在交通规划领域创造与政策相关的成果。我们将使用一个简单的方法(出于教育目的)来识别可持续交通基础设施投资的有希望的地点。

本章概述的数据驱动方法的一个优点是其模块性:每个方面本身就很有用,并可以融入更广泛的分析中。使我们达到这一阶段的步骤包括在路线节中识别短途但依赖汽车的通勤路线(由欲望线生成),以及使用sfnetworks包在路线网络中分析路由网络特性。本章的最后一个代码块将这些分析纵向结合,通过在代表距离自行车基础设施很近的区域的新数据集上覆盖上一节中的自行车潜力估计。

1
2
3
4
existing_cycleways_buffer = bristol_ways |> 
filter(highway == "cycleway") |> # 1) filter out cycleways
st_union() |> # 2) unite geometries
st_buffer(dist = 100) # 3) create buffer

下一步是创建一个数据集,该数据集代表网络上存在高自行车潜力但几乎没有为自行车提供便利设施的点:

1
2
3
4
5
route_network_no_infra = st_difference(
route_network_scenario,
route_network_scenario |> st_set_crs(st_crs(existing_cycleways_buffer)),
existing_cycleways_buffer
)
1
2
#> Warning: attribute variables are assumed to be spatially constant throughout
#> all geometries

前面代码块的结果显示在下图中,该图显示了具有高度依赖汽车、具有高自行车潜力但没有自行车道的路线。

1
2
3
tmap_mode("view")
qtm(route_network_no_infra, basemaps = leaflet::providers$Esri.WorldTopoMap,
lines.lwd = 5)
1
2
#> Warning: Some legend items or map compoments do not fit well (e.g. due to the
#> specified font size).


Potential routes along which to prioritise cycle infrastructure in Bristol to reduce car dependency. The static map provides an overview of the overlay between existing infrastructure and routes with high car-bike switching potential (left). The screenshot the interactive map generated from the qtm() function highlights Whiteladies Road as somewhere that would benefit from a new cycleway (right).

该方法存在一些局限性:实际上,人们并不总是前往区域中心或始终使用特定模式的最短路径算法。然而,结果展示了如何使用地理数据分析来突出可能特别有益于新投资自行车道的地方,尽管这种方法很简单。为了在实践中指导交通规划设计,这种分析需要大幅扩展——包括使用更大的输入数据集。

旅行的未来方向

这一章提供了使用地理计算进行交通研究的可能性的一个概览,并使用开放数据和可复制代码探讨了构成城市交通系统的一些关键地理元素。这些结果有助于规划需要投资的地方。

交通系统在多个相互作用的层面上运行,这意味着地理计算方法有很大的潜力,可以生成有关它们如何运作以及不同干预措施可能产生的影响的见解。在这一领域还有更多可以做的事情:基于本章提供的基础,有很多方向可以进行拓展。

交通是许多国家温室气体排放增长最快的来源,并将成为“尤其是在发达国家中最大的温室气体排放部门”。交通相关的排放在社会上分布不均,但(与食物和供暖不同)对幸福来说并非必需品。通过需求减少、车队电气化以及积极采取像步行和骑自行车这样的活动出行方式,该部门有很大的快速减碳潜力。新技术可以通过更多的汽车共享来减少对汽车的依赖。诸如无桩自行车和电动滑板车方案这样的“微移动”系统也正在出现,以General Bikeshare Feed Specification(GBFS)格式创建有价值的数据集,可以用gbfs包进行导入和处理。

从方法论的角度看,本章提供的基础可以通过在分析中包括更多变量来进行扩展。例如,通过使用R语言的统计建模能力,这可以用来预测当前和未来的自行车使用水平。

这种类型的分析是Propensity to Cycle Tool(PCT)的基础,这是一个用R语言开发的,用于优先考虑英格兰各地自行车投资的公开可访问的映射工具(lovelace 2017)。类似的工具可以用于鼓励与空气污染和公共交通准入等其他主题相关的基于证据的交通政策。

练习

E1. 在本章的大部分分析中,我们关注的是主动模式,但是驾驶行程呢?

  • desire_lines对象中,有多大比例的行程是通过驾驶完成的?
  • 有多大比例的desire_lines的直线长度为5公里或更远?
  • 在长度超过5公里的desire_lines中,有多大比例的行程是通过驾驶完成的?
  • 绘制那些长度小于5公里且其中超过50%的行程是通过汽车完成的desire_lines
  • 你注意到这些依赖汽车但desire_lines较短的地方有什么特点?

E2. 如果在离现有自行车道超过100米的区段上建造了上图中展示的所有路线,会增加多少长度的自行车道?

E3. desire_lines中表示的行程有多大比例在routes_short_scenario对象中得到了体现?

  • 奖励:所有行程中有多大比例发生在穿越routes_short_scenariodesire_lines上?

E4. 本章展示的分析旨在教授如何将地理计算方法应用于交通研究。
如果你是在政府或交通咨询公司中真正进行这项工作,你会有哪三个不同的做法?

E5. 显然,上图中识别的路线只是部分情况。
你会如何扩展这个分析?

E6. 假设你想通过创建投资基于场所的自行车政策的关键区域(而不是路线)来扩展场景,比如无车区、自行车停车点和减少汽车停车策略。
栅格数据集如何协助完成这项工作?

  • 奖励:开发一个光栅层,将布里斯托地区划分为100个单元格(10x10),并从bristol_ways数据集(参见章节 @ref(location))估算每个单元格中道路的平均速度限制。

本文由SCY翻译自《Geocomputation with R》第十二章

前提条件

本章假设您已经具备地理数据分析的熟练技能,例如通过学习并完成章节空间数据地理数据重投影中的练习来获得。强烈推荐熟悉广义线性模型(GLM)和机器学习。

本章使用以下的R包:^[虽然不需要附加,但还必须安装GGallylgrkernlabmlr3measuresparadoxpROCprogressrspDataLarge包。]

1
2
3
4
5
6
7
8
9
10
11
12
library(sf)
library(terra)
library(dplyr)
library(future)
library(lgr)
library(mlr3)
library(mlr3learners)
library(mlr3extralearners)
library(mlr3spatiotempcv)
library(mlr3tuning)
library(mlr3viz)
library(progressr)

当然,所需数据将在适当的时候附上。

引言

统计学习主要关注使用统计和计算模型来识别数据中的模式,并根据这些模式进行预测。由于其起源,统计学习是 R 语言的一大优势(见章节地理计算软件)。^[几十年来,将统计技术应用于地理数据一直是地统计学、空间统计学和点模式分析领域中的一个活跃的研究课题。统计学习结合了统计和机器学习的方法,并可分为监督和无监督技术。这两者越来越多地应用于从物理学、生物学和生态学到地理学和经济学等各个学科]。

本章重点介绍有训练数据集的监督技术,而非像聚类这样的无监督技术。响应变量可以是二元的(如山体滑坡发生与否)、分类的(土地利用)、整数(物种丰富度计数)或数值的(以 pH 衡量的土壤酸度)。监督技术用于建模——响应与一个或多个预测变量之间的关系——这些响应是基于一组观测样本而已知的。

大多数机器学习研究的主要目的是进行良好的预测。机器学习在“大数据”时代蓬勃发展,因为其方法对输入变量几乎没有假设,并且能处理巨大的数据集。机器学习适用于预测未来客户行为、推荐服务(音乐、电影、接下来购买什么)、面部识别、自动驾驶、文本分类和预测性维护(基础设施、产业)等任务。

本章基于一个案例研究:(空间)预测山体滑坡。
该应用与定义在引言章中的地理计算的应用性有关,并说明了机器学习在唯一目的是预测时如何借鉴统计学领域。因此,本章首先使用广义线性模型介绍建模和交叉验证的概念。在此基础上,该章实施了一个更典型的机器学习算法,即支持向量机(SVM)。模型的预测性能将使用空间交叉验证(CV)进行评估,该方法考虑到地理数据是特殊的。

CV确定模型泛化到新数据的能力,通过将数据集(反复)分割为训练集和测试集。它使用训练数据来拟合模型,并检查其在对测试数据进行预测时的性能。CV 有助于检测过拟合,因为过于紧密地预测训练数据(噪声)的模型通常会在测试数据上表现不佳。

随机分割空间数据可能导致训练点与测试点在空间上是邻近的。由于空间自相关,在这种情况下,测试数据集和训练数据集将不是独立的,因此交叉验证无法检测出可能的过拟合。空间交叉验证缓解了这个问题,并且是本章的核心主题。

案例: 滑坡敏感性

本案例研究基于南厄瓜多尔滑坡地点的一个数据集,如下图所示,并在@muenchow_geomorphic_2012中详细描述。该论文中使用的数据集的一个子集提供在spDataLarge包中,可以如下加载:

1
2
data("lsl", "study_mask", package = "spDataLarge")
ta = terra::rast(system.file("raster/ta.tif", package = "spDataLarge"))

T上面的代码加载了三个对象:一个名为lsldata.frame,一个名为study_masksf对象,以及一个名为taSpatRaster,其中包含地形属性栅格。lsl包含一个因子列lslpts,其中TRUE对应于观察到的滑坡’发起点’,坐标存储在列xy中。^[滑坡发起点位于滑坡多边形的断崖中。有关进一步的详细信息。]
有175个滑坡点和175个非滑坡点,如summary(lsl$lslpts)所示。这175个非滑坡点是从研究区域随机抽样的,但必须落在滑坡多边形周围的小缓冲区之外。


Landslide initiation points (red) and points unaffected by landsliding (blue) in Southern Ecuador.

lsl的前三行,四舍五入为两位有效数字,可以在下表中找到。

Structure of the lsl dataset.
x y lslpts slope cplan cprof elev log10_carea
1 713888 9558537 FALSE 34 0.023 0.003 2400 2.8
2 712788 9558917 FALSE 39 -0.039 -0.017 2100 4.1
350 713826 9559078 TRUE 35 0.020 -0.003 2400 3.2

要对滑坡易发性进行建模,我们需要一些预测因子。由于地形属性经常与滑坡有关[@muenchow_geomorphic_2012],我们已经从ta提取到lsl中以下的地形属性:

  • slope:坡度角(°)
  • cplan:平面曲率(rad m^−1^)表示坡度的汇聚或发散,从而表示水流
  • cprof:剖面曲率(rad m^-1^)作为流速加速的衡量标准,也称为坡度角的下坡变化
  • elev:海拔高度(m a.s.l.)作为研究区不同海拔带植被和降水的表示
  • log10_carea:集水区面积(log10 m^2^)的常用对数,表示流向某个位置的水量

使用R-GIS桥计算地形属性并将其提取到滑坡点(参见本章末尾的练习部分)可能是一个值得尝试的练习。

在R中的传统建模方法

在介绍mlr3包之前,这是一个提供统一接口以访问数十种学习算法的 umbrella 包,值得先看一下R传统的建模接口。这一介绍到有监督的统计学习为进行空间CV提供了基础,并有助于更好地理解随后呈现的mlr3方法。

监督学习涉及将响应变量作为预测因子的函数进行预测。在R中,建模函数通常使用公式来指定(有关R公式的更多详细信息,请参阅?formula)。以下命令指定并运行一个广义线性模型:

1
2
3
fit = glm(lslpts ~ slope + cplan + cprof + elev + log10_carea,
family = binomial(),
data = lsl)

理解这三个输入参数各自的意义是非常有价值的:

  • 一个公式,用于指定由预测因子决定的滑坡发生(lslpts
  • 一个家族(family),用于指定模型的类型,在这个例子中是binomial,因为响应是二元的(见?family
  • 包含响应和预测因子(作为列)的数据框

可以如下打印这个模型的结果(summary(fit)提供了关于结果的更详细的描述):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class(fit)
#> [1] "glm" "lm"
fit
#>
#> Call: glm(formula = lslpts ~ slope + cplan + cprof + elev + log10_carea,
#> family = binomial(), data = lsl)
#>
#> Coefficients:
#> (Intercept) slope cplan cprof elev log10_carea
#> 2.511364 0.079011 -28.941961 -17.563601 0.000179 -2.274877
#>
#> Degrees of Freedom: 349 Total (i.e. Null); 344 Residual
#> Null Deviance: 485
#> Residual Deviance: 373 AIC: 385

模型对象fit,其类别为glm,包含了定义响应和预测因子之间拟合关系的系数。这个对象也可以用于预测。这是通过通用的predict()方法来完成的,在这个案例中,它调用了predict.glm()函数。将type设置为response会返回lsl中每个观测的预测概率(即滑坡发生的概率),如下图所示(见?predict.glm):

1
2
3
4
pred_glm = predict(object = fit, type = "response")
head(pred_glm)
#> 1 2 3 4 5 6
#> 0.1901 0.1172 0.0952 0.2503 0.3382 0.1575

通过将系数应用于预测因子的栅格数据,可以创建空间分布图。这可以手动完成,也可以使用terra::predict()来完成。除了一个模型对象(fit)外,这个函数还需要一个具有预测因子(栅格层)的SpatRaster,这些预测因子的名称应与模型输入数据框中的名称相同(见下图)。

1
2
# making the prediction
pred = terra::predict(ta, model = fit, type = "response")


Spatial distribution mapping of landslide susceptibility using a GLM.

在进行预测时,我们忽略了空间自相关,因为我们假设平均预测准确性在有或没有空间自相关结构的情况下保持不变。然而,也可以将空间自相关结构纳入模型和预测中。
尽管这超出了本书的范围,但我们为感兴趣的读者提供了一些查阅的方向:

  1. 回归克里金(Regression Kriging)的预测结合了回归的预测和回归残差的克里金。
  2. 人们还可以向广义最小二乘模型[nlme::gls()]添加空间相关(依赖)结构。
  3. 人们也可以使用混合效应建模方法。

基本上,随机效应对响应变量施加了依赖结构,从而使某一类的观察结果与另一类的观察结果更相似。类别可以是蜜蜂巢、猫头鹰巢、植被样带或高度分层。这种混合建模方法假设随机截距是正态和独立分布的。这甚至可以通过使用正态和空间依赖的随机截距来扩展。然而,为此,你可能需要采用贝叶斯建模方法,因为频率论软件工具在这方面尤其有限,尤其是对于更复杂的模型。

空间分布图是模型(上图)的一个非常重要的结果。更重要的是,基础模型在制作这些图时有多好,因为如果模型的预测性能差,预测图就毫无用处。评估二项模型预测性能的最流行指标是接收器操作特性曲线下的面积(AUROC)。这是一个介于0.5和1.0之间的值,其中0.5表示模型不比随机模型好,1.0表示两个类的完美预测。因此,AUROC越高,模型的预测能力越好。以下代码块使用roc()计算模型的AUROC值,该函数将响应和预测值作为输入。auc()返回曲线下的面积。

1
2
pROC::auc(pROC::roc(lsl$lslpts, fitted(fit)))
#> Area under the curve: 0.8216

AUROC\index{AUROC} 值为

0.82 代表了一个良好的拟合。然而,这是一个过于乐观的估计,因为我们是在完整数据集上进行的计算。为了得到一个偏差较小的评估,我们需要使用交叉验证,并且在处理空间数据的情况下,应使用空间CV。

介绍(空间)交叉验证

交叉验证属于重采样方法的家族 [@james_introduction_2013]。基本思想是将数据集(反复)分为训练集和测试集,其中训练数据用于拟合模型,然后将该模型应用于测试集。通过使用如二项式案例中的AUROC这样的性能度量,将预测值与测试集中已知的响应值进行比较,从而得到模型将学习到的关系泛化到独立数据的能力的偏差较小的评估。例如,100次重复的5折交叉验证意味着将数据随机分为五个部分(折叠),每个折叠都被用作一次测试集(见下图的上行)。这保证了每个观测值都在测试集中使用一次,并需要拟合五个模型。随后,这一过程重复100次。当然,每次重复中的数据分割都会有所不同。总体而言,这合计为500个模型,而所有模型的平均性能度量(AUROC)即为模型的整体预测能力。

然而,地理数据是特殊的。正如我们将在运输章节中看到的,地理学的“第一定律”指出,彼此靠近的点通常比远离的点更相似 [@miller_tobler_2004]。这意味着这些点在统计上并不独立,因为在传统的CV中,训练点和测试点通常过于靠近(见下图的第一行)。靠近“测试”观测值的“训练”观测值可能提供一种“偷窥预览”:这些信息本应对训练数据集不可用。为了缓解这一问题,“空间划分”被用于将观测值分为空间上不相交的子集(使用观测值的坐标进行k-均值聚类;@brenning_spatial_2012;见下图的第二行)。这种划分策略是空间CV和传统CV之间的唯一差异。因此,空间CV导致了对模型预测性能的偏差较小的评估,从而有助于避免过拟合。


Spatial visualization of selected test and training observations for cross-validation of one repetition. Random (upper row) and spatial partitioning (lower row).

使用mlr3进行空间交叉验证

CRAN 机器学习任务视图所描述的,有数十个用于统计学的包。熟悉其中每一个包,包括如何进行交叉验证和超参数调整,可能是一个耗时的过程。比较来自不同包的模型结果可能更为繁琐。mlr3 包和生态系统是为了解决这些问题而开发的。它充当一个“元包”,为包括分类、回归、生存分析和聚类在内的流行的有监督和无监督统计学习技术提供统一的接口 [@lang_mlr3_2019; @becker_mlr3_2022]。标准化的 mlr3 接口基于八个“构建块”。
如下图所示,这些构建块有明确的顺序。

(ref:building-blocks) mlr3包的基础构建块。来源:@becker_mlr3_2022。(得到了友好地重新使用此图的许可。)


(ref:building-blocks)

mlr3 建模过程包含三个主要阶段。
首先,**任务(task)**指定了数据(包括响应变量和预测变量)和模型类型(如回归或分类)。
其次,学习器(learner) 定义了应用于创建的任务的特定学习算法。
第三,**重采样(resampling)**方法评估了模型的预测性能,即其泛化到新数据的能力。

广义线性模型

要在 mlr3中实现GLM,我们必须创建一个包含滑坡数据的任务(task)。由于响应是二元(两类变量)且具有空间维度,我们使用 mlr3spatiotempcv 包的TaskClassifST$new()创建一个分类任务[@schratz_mlr3spatiotempcv_2021,对于非空间任务,使用 mlr3::TaskClassif$new() 或对于回归任务使用mlr3::TaskRegr$new(),其他任务类型请参见 ?Task]。^[mlr3 生态系统大量使用 data.tableR6 类。尽管你可能在不了解 data.tableR6 的具体情况下使用 mlr3,但了解它们可能相当有帮助。要了解更多关于 data.table 的信息,请参考 https://rdatatable.gitlab.io/data.table/index.html。要了解更多关于 R6,我们推荐阅读《Advanced R》书的第14章 [@wickham_advanced_2019]。]
这些 Task*$new() 函数的第一个重要参数是 backendbackend 期望输入数据包括响应变量和预测变量。target 参数指示响应变量的名称(在我们的例子中是 lslpts),而 positive 决定响应变量的两个因子水平中哪一个表示滑坡起点(在我们的例子中是 TRUE)。lsl 数据集的所有其他变量将作为预测因子。对于空间交叉验证,我们需要提供一些额外的参数。coordinate_names 参数期望坐标列的名称(参见空间交叉验证章节和图3)。另外,我们应该指示使用的坐标参考系统(crs)并决定是否希望在建模中使用坐标作为预测因子(coords_as_features)。

1
2
3
4
5
6
7
8
9
10
# create task
task = mlr3spatiotempcv::TaskClassifST$new(
id = "ecuador_lsl",
backend = mlr3::as_data_backend(lsl),
target = "lslpts",
positive = "TRUE",
coordinate_names = c("x", "y"),
coords_as_features = FALSE,
crs = "EPSG:32717"
)

注意,mlr3spatiotempcv::as_task_classif_st() 也接受一个 sf-对象作为 backend 参数的输入。在这种情况下,你可能只想额外指定 coords_as_features 参数。我们没有将 lsl 转换为一个 sf-对象,因为 TaskClassifST$new() 在背景中会将其转换回一个非空间的 data.table 对象。对于简短的数据探索,mlr3viz 包的 autoplot() 函数可能会很方便,因为它会绘制响应与所有预测因子的关系,以及所有预测因子之间的关系(未显示)。

1
2
3
4
# plot response against each predictor
mlr3viz::autoplot(task, type = "duo")
# plot all variables against each other
mlr3viz::autoplot(task, type = "pairs")

创建了任务之后,我们需要选择一个学习器(learner),以确定要使用的统计学\index{statistical learning}方法。所有分类的学习器classif. 开头,所有回归的学习器以 regr. 开头(详见 ?Learner)。mlr3extralearners::list_mlr3learners() 列出了所有可用的学习器以及 mlr3 从哪个软件包中导入了它们(表 @ref(tab:lrns))。要了解能够对二元响应变量进行建模的学习器,我们可以运行:

1
2
3
4
mlr3extralearners::list_mlr3learners(
filter = list(class = "classif", properties = "twoclass"),
select = c("id", "mlr3_package", "required_packages")) |>
head()
Sample of available learners for binomial tasks in the mlr3 package.
Class Name Short name Package
classif.adaboostm1 ada Boosting M1 adaboostm1 RWeka
classif.binomial Binomial Regression binomial stats
classif.featureless Featureless classifier featureless mlr
classif.fnn Fast k-Nearest Neighbour fnn FNN
classif.gausspr Gaussian Processes gausspr kernlab
classif.IBk k-Nearest Neighbours ibk RWeka

这将产生所有能够处理两类问题(有或没有滑坡)的学习器。我们选择在传统建模节中使用的二项分类方法,该方法在 mlr3learners 中实现为 classif.log_reg。另外,我们需要指定 predict.type,它决定了预测的类型,prob 会得出滑坡发生概率在0到1之间的预测值(这对应于 predict.glm 中的 type = response)。

1
learner = mlr3::lrn("classif.log_reg", predict_type = "prob")

要访问学习器的帮助页面并找出它来自哪个包,我们可以运行:

1
learner$help()
1
2
# performance estimation level
perf_level = mlr3::rsmp("repeated_spcv_coords", folds = 5, repeats = 100)

请注意,这与节glm中用于GLM的重采样所用的代码完全相同;我们只是在这里重复它作为提醒。

到目前为止,这个过程与节glm中描述的过程相同。然而,下一步是新的:调整超参数。使用相同的数据进行性能评估和调优可能会导致过于乐观的结果[@cawley_overfitting_2010]。这可以通过使用嵌套的空间交叉验证(CV)来避免。


Schematic of hyperparameter tuning and performance estimation levels in CV. [Figure was taken from Schratz et al. (2019). Permission to reuse it was kindly granted.]

这意味着我们将每个折叠再次分成五个空间上不重叠的子折叠,这些子折叠用于确定最佳的超参数\index{hyperparameter}(下面代码块中的tune_level对象;见上图以获取视觉表示)。为了找到最优的超参数组合,我们在这些子折叠中分别拟合50个模型(下面代码块中的terminator对象),并为超参数C和Sigma随机选择值。C和Sigma的随机选择值还受到预定义调优空间(search_space对象)的额外限制。调优空间的范围是根据文献中推荐的值来选择的[@schratz_hyperparameter_2019]。

1
2
3
4
5
6
7
8
9
10
# five spatially disjoint partitions
tune_level = mlr3::rsmp("spcv_coords", folds = 5)
# use 50 randomly selected hyperparameters
terminator = mlr3tuning::trm("evals", n_evals = 50)
tuner = mlr3tuning::tnr("random_search")
# define the outer limits of the randomly selected hyperparameters
search_space = paradox::ps(
C = paradox::p_dbl(lower = -12, upper = 15, trafo = function(x) 2^x),
sigma = paradox::p_dbl(lower = -15, upper = 6, trafo = function(x) 2^x)
)

下一步是根据所有定义超参数调优的特性,使用AutoTuner$new()来修改学习器lrn_ksvm

1
2
3
4
5
6
7
8
at_ksvm = mlr3tuning::AutoTuner$new(
learner = lrn_ksvm,
resampling = tune_level,
measure = mlr3::msr("classif.auc"),
search_space = search_space,
terminator = terminator,
tuner = tuner
)

现在调优设置已完成,用于确定一个折叠的最佳超参数将拟合250个模型。重复这个过程针对每个折叠,我们最终得到125,000(250 * 5)个模型用于每次重复。重复100次意味着拟合总共125,000个模型以确定最佳超参数(见图)。这些将用于性能评估,这需要拟合另外500个模型(5个折叠 * 100次重复;参见图。
为了更清晰地描述性能评估处理链,让我们记录下我们给计算机的命令:

  1. 性能层次(图的左上部分)- 将数据集分成五个空间上不交叠的(外部)子折叠。
  2. 调优层次(图的左下部分)- 使用性能层次的第一个折叠,并再次将其空间上分成五个(内部)子折叠进行超参数调优。在这些内部子折叠中使用50个随机选定的超参数,即拟合250个模型。
  3. 性能估计 - 使用上一步(调优层次)中的最佳超参数组合,并将其应用于性能层次中的第一个外部折叠以估计性能(AUROC)。
  4. 重复步骤2和3,针对其余四个外部折叠。
  5. 重复步骤2至4,共100次。

超参数调优和性能估计的过程在计算上是非常密集的。为了减少模型运行时间,mlr3提供了使用future包进行并行化\index{parallelization}的可能性。由于我们即将进行嵌套交叉验证,我们可以决定是否希望并行化内部循环或外部循环(见图的左下部分)。由于前者将运行125,000个模型,而后者仅运行500个,很明显我们应该并行化内部循环。要设置内部循环的并行化,我们执行:

1
2
3
4
library(future)
# execute the outer loop sequentially and parallelize the inner loop
future::plan(list("sequential", "multisession"),
workers = floor(availableCores() / 2))

确实,我们指示future包仅使用可用核心的一半而非全部(默认设置),这样做是为了在使用高性能计算集群的情况下允许其他可能的用户也能在同一个集群上工作。

现在,我们已经为执行嵌套空间交叉验证做好了准备。指定resample()参数的过程与使用GLM时完全相同,唯一的区别是store_modelsencapsulate参数。将前者设置为TRUE将允许我们提取超参数调优结果,这在我们计划对调优进行后续分析时非常重要。后者确保即使其中一个模型出错,处理过程也会继续。这避免了由于一个失败的模型而导致整个过程停止,这在大规模模型运行中是非常需要的。一旦处理完成,可以查看失败的模型。处理完成后,最好使用future:::ClusterRegistry("stop")明确地停止并行化。最后,我们将输出对象(result)保存到磁盘,以防我们希望在另一个R会话中使用它。

在运行以下代码之前,请注意,由于它将执行包括125,500个模型在内的空间交叉验证,所以这是一个耗时的过程。在现代笔记本电脑上轻松运行半天也是可能的。需要注意的是,运行时间取决于多个方面:CPU速度、所选算法、所选核心数以及数据集。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
progressr::with_progress(expr = {
rr_spcv_svm = mlr3::resample(task = task,
learner = at_ksvm,
# outer resampling (performance level)
resampling = perf_level,
store_models = FALSE,
encapsulate = "evaluate")
})

# stop parallelization
future:::ClusterRegistry("stop")
# compute the AUROC values
score_spcv_svm = rr_spcv_svm$score(measure = mlr3::msr("classif.auc"))
# keep only the columns you need
score_spcv_svm = score_spcv_svm[, .(task_id, learner_id, resampling_id, classif.auc)]

如果你不想在本地运行代码,我们已经将 score_svm 保存在本书的GitHub仓库中。你可以按照以下方式加载它们:

1
2
3
score = readRDS("extdata/12-bmr_score.rds")
score_spcv_svm = score[learner_id == "classif.ksvm.tuned" &
resampling_id == "repeated_spcv_coords"]

让我们看一下最终的AUROC:模型区分两个类别的能力。

1
2
3
# final mean AUROC
round(mean(score_spcv_svm$classif.auc), 2)
#> [1] 0.74

在这个特定情况下,似乎GLM(汇总的AUROC是 r score[resampling_id == "repeated_spcv_coords" & learner_id == "classif.log_reg", round(mean(classif.auc), 2)])比SVM略好一些。为了保证一个绝对公平的比较,还应确保两个模型使用完全相同的划分——这是我们在这里没有展示,但在背后默默使用的(更多信息请参见本书GitHub仓库中的code/12_cv.R)。为此,mlr3提供了benchmark_grid()benchmark()函数[参见https://mlr3book.mlr-org.com/perf-eval-cmp.html#benchmarking,@becker_mlr3_2022]。我们将在练习中更详细地探讨这些函数。还请注意,在SVM的随机搜索中使用超过50次迭代可能会产生导致模型具有更好AUROC的超参数\index{hyperparameter} [@schratz_hyperparameter_2019]。另一方面,增加随机搜索迭代次数也会增加总模型数量,从而增加运行时间。

到目前为止,空间CV已被用于评估学习算法泛化到未见数据的能力。对于预测性制图目的,人们会在完整的数据集上调整超参数。这将在生态章中讨论。

结论

重采样方法是数据科学家工具箱中的一个重要组成部分[@james_introduction_2013]。本章使用交叉验证来评估各种模型的预测性能。具有空间坐标的观测值可能由于空间自相关而不是统计上独立的,这违反了交叉验证的一个基本假设。空间CV通过减少空间自相引入的偏见来解决这个问题。

mlr3软件包便于使用(空间)重采样技术,结合最受欢迎的统计学习技术,包括线性回归,如广义加性模型之类的半参数模型,以及如随机森林,SVMs和增强回归树[@bischl_mlr:_2016;@schratz_hyperparameter_2019]等机器学习技术。机器学习算法通常需要超参数输入,其中最优的“调整”可能需要数千次模型运行,这需要大量的计算资源,消耗大量时间、RAM和/或核心。mlr3通过启用并行化来解决这个问题。

总体而言,机器学习及其用于理解空间数据是一个大领域,本章提供了基础知识,但还有更多需要学习。我们推荐以下方向的资源:

练习

E1. 使用R-GIS桥梁(参见GIS软件桥梁章节),从通过terra::rast(system.file("raster/ta.tif", package = "spDataLarge"))$elev加载的elev数据集中计算以下地形属性:

  • 坡度
  • 平面曲率
  • 轮廓曲率
  • 流域面积

E2. 从相应的输出栅格中提取值到lsl数据框(通过data("lsl", package = "spDataLarge")),并添加名为slopecplancprofelevlog_carea的新变量。

E3. 结合GLM使用派生的地形属性栅格,制作一个与图12.2中显示的相似的空间预测地图。
运行data("study_mask", package = "spDataLarge")将学习区域的掩码附加上。

E4. 基于GLM学习器计算100次重复的5折非空间交叉验证和空间CV,并利用箱线图比较两种重采样策略的AUROC值。

提示:你需要指定一个非空间重采样策略。

另一个提示:你可能想借助mlr3::benchmark()mlr3::benchmark_grid()(更多信息,请参考https://mlr3book.mlr-org.com/performance.html#benchmarking)一次性解决练习4至6。
在做此操作时,请记住计算可能需要很长时间,可能需要几天。
这当然取决于你的系统。
你拥有的RAM和核心越多,计算时间就会越短。

E5. 使用二次判别分析(QDA)对滑坡敏感性进行建模。
评估QDA的预测性能。
QDA和GLM的空间交叉验证平均AUROC值之间有什么区别?

E6. 不调整超参数运行SVM。
使用$\sigma$ = 1 和 C = 1的rbfdot核。
否则,在kernlabksvm()中不指定超参数将初始化自动的非空间超参数调整。

本文由SCY翻译自《Geocomputation with R》第十一章

前提条件

本章前提条件最小,因为它主要使用基础的R语言。sf包用于检查我们将开发的算法的结果,该算法用于计算多边形的面积。就先前的知识而言,本章假设您已经理解了R中的地理数据中介绍的地理类,以及如何使用它们来表示各种各样的输入文件格式(参见地理数据I/O)。

引言

引言章阐明了地理计算不仅仅是使用现有的工具,还包括以“可共享的R脚本和函数”的形式开发新工具。本章教授可复现代码的这些基础构件。它还介绍了低级几何算法,这种算法在GIS软件桥梁章中有应用。阅读本章应有助于您理解这类算法是如何工作的,并编写可多次、由多人、在多个数据集上使用的代码。然而,单凭本章自身无法使您成为一名熟练的程序员。编程是困难的,需要大量的实践。

要理解编程作为一种自成体系的智力活动,您必须转向计算机编程;您必须阅读和编写计算机程序——很多程序。

有充分的理由去学习编程。虽然本章本身并未教授编程——参见多种资源,它们教授R语言和其他语言的编程——但它确实提供了一些着眼于几何数据的起点,这些起点可能是发展编程技能的良好基础。

本章还演示并强调了可重复性的重要性。可重复性的优点不仅仅是允许其他人复制您的工作:相对于只运行一次的代码,可重复的代码在各方面通常都更优秀,包括计算效率、‘可扩展性’(代码在大数据集上运行的能力)以及更容易进行适应和维护。

脚本是可重复R代码的基础,这一主题在脚本节中有覆盖。算法是一系列用于修改输入并得出输出的步骤的配方,如几何算法节所述。为了简化共享和可重复性,算法可以被放置在函数中。这是函数节的主题。找出多边形的质心的例子将用于整合这些概念。几何操作章已经介绍了一个质心函数st_centroid(),但这个例子突出显示了看似简单的操作其实是相对复杂代码的结果,这证实了以下的观察:

关于空间数据问题最吸引人的一点是,对人来说看似极其简单的事情,在计算机上可能出奇地困难。

这个例子也反映了本章的次要目标,即不是“复制现有的内容,而是展示现有内容是如何工作的”。

脚本

如果说包中分发的函数是R代码的基础构件,那么脚本就是将它们粘合在一起的胶水。脚本应该按照逻辑顺序存储和执行,以创建可复现的工作流,这可以手动完成,也可以使用诸如targets这样的工作流自动化工具来完成。如果你是编程新手,当你第一次遇到脚本时,它们可能看起来让人望而却步,但实际上它们只是普通的文本文件。脚本通常保存为一个扩展名代表它们包含的语言的文件,例如,用Python编写的脚本使用.py扩展名,用Rust编写的脚本使用.rs扩展名。R脚本应该保存为.R扩展名,并且名字应该反映它们的功能。一个例子是11-hello.R,这是一个存储在书籍仓库的code文件夹中的脚本文件。11-hello.R是一个非常简单的脚本,只包含两行代码,其中一行是注释。

这些脚本通常是科研过程中非常有用的组成部分,特别是当你需要对大量栅格和矢量数据进行处理和后续分析时,它们可以帮助你实现高度自动化和可复现的工作流程。而使用例如targets这样的工作流工具,可以进一步提高代码的结构性和可维护性。

1
2
# Aim: provide a minimal R script
print("Hello geocompr")

这个脚本的内容并不特别令人兴奋,但它说明了一点:脚本不需要复杂。保存的脚本可以通过R命令行中的source()函数完整地调用和执行,如下面所示。这个命令的输出显示,注释会被忽略,但print()命令会被执行:

1
2
source("code/11-hello.R")
#> [1] "Hello geocompr"

你也可以从系统命令行shell,例如bashPowerShell,调用R脚本,前提是RScript可执行文件已经配置好,例如如下:

1
Rscript code/11-hello.R

关于脚本文件中可以和不能包含什么,并没有严格的规定,也没有什么能阻止你保存错误的、不可复制的代码。不包含有效R代码的代码行应该被注释掉,通过在行的开始处添加#,以防止错误,如11-hello.R脚本中的第一行所示。然而,有一些值得遵循的惯例:

  • 按顺序编写脚本:就像电影的剧本一样,脚本应该有一个明确的顺序,比如’设置’、‘数据处理’和’保存结果’(大致相当于电影中的’开头’、‘中间’和’结尾’)。
  • 向脚本添加注释,以便其他人(以及你未来的自己)能够理解它。
    至少,注释应该说明脚本的目的(见图@ref(fig:codecheck))并(对于长脚本)将其划分为几个部分。
    这可以在RStudio\index{RStudio}中通过快捷键Ctrl+Shift+R来完成,它会创建’可折叠’的代码段标题。
  • 最重要的是,脚本应该是可复制的:能在任何计算机上运行的自包含脚本比只能在你的计算机上、在好天气下运行的脚本更有用。
    这涉及到在开始时附加所需的包,从持久性来源(如可靠网站)读取数据,并确保已经采取了之前的步骤。^[
    之前的步骤可以用注释或者if语句来指出,比如if (!exists("x")) source("x.R")(如果对象x不存在,则会运行脚本文件x.R)。
    ]

在R脚本中强制可复制性是困难的,但有一些工具可以帮助。默认情况下,RStudio \index{RStudio} 会’代码检查’ R脚本,并用红色波浪线标出有问题的代码,如下图所示:


Code checking in RStudio. This example, from the script 11-centroid-alg.R, highlights an unclosed curly bracket on line 19.

📌A useful tool for reproducibility is the reprex package.
Its main function reprex() tests lines of R code to check if they are reproducible, and provides markdown output to facilitate communication on sites such as GitHub.
See the web page reprex.tidyverse.org for details.

本节的内容适用于任何类型的R脚本。对于地理计算脚本的特殊考虑是,它们往往具有外部依赖性,例如在第@ref(read-write)章中重度使用的用于处理地理数据的核心R包所需的GDAL依赖,用于数据导入和导出。运行更专业的地理算法可能需要GIS软件依赖,如第十章所概述。处理地理数据的脚本还常常要求输入数据集以特定格式提供。这种依赖应在脚本的注释中或其所属项目的其他地方进行说明,如脚本 11-centroid-alg.R 所示。‘防御性’ 编程技巧和良好的错误信息可以通过检查依赖性和与用户沟通(如果某些需求未得到满足)来节省时间。如果语句,用R中的if ()实现,可用于发送消息或运行代码行,只有在满足某些条件时才会这样做。例如,以下代码行会在缺少某个文件时向用户发送消息:

1
2
3
4
if (!file.exists("required_geo_data.gpkg")) {
message("No file, required_geo_data.gpkg is missing!")
}
#> No file, required_geo_data.gpkg is missing!

11-centroid-alg.R脚本所进行的工作在下面的可复制示例中有所展示,该示例创建了一个名为poly_mat的预备对象,代表一个边长为9单位的正方形。这个例子显示了source()可以与URLs一同工作,假设你有互联网连接。如果没有,相同的脚本可以通过source("code/11-centroid-alg.R")来调用,前提是你之前已经下载了github.com/geocompx/geocompr仓库,并且你正在geocompr文件夹中运行R。

1
2
3
4
5
6
poly_mat = cbind(
x = c(0, 9, 9, 0, 0),
y = c(0, 0, 9, 9, 0)
)
# Short URL to code/11-centroid-alg.R in the geocompr repo
source("https://t.ly/0nzj")
1
2
#> [1] "The area is: 81"
#> [1] "The coordinates of the centroid are: 4.5, 4.5"

几何算法

我们可以这么理解算法,它相当于烘焙食谱。它们是一整套指导方针,当对输入进行操作时,会得到有用/美味的结果。输入在烘焙的情况下是如面粉和糖的成分,在算法的情况下是数据和输入参数。而烘焙食谱可能导致美味的蛋糕,成功的算法应有带来环境/社会/其他利益的计算结果。在深入可复制示例之前,下面简短的历史将展示算法是如何与脚本(在脚本节中涉及)和函数(可以用来概括算法,使其更便携和易于使用,我们将在函数节中看到)相关联的。

"算法"这个词源自9世纪在巴格达出版的早期数学教科书 Hisab al-jabr w’al-muqabala。这本书被译成拉丁文,并变得非常流行,以至于作者的姓氏,al-Khwārizmī,“被永久地当作一个科学术语来记忆:Al-Khwarizmi 变成了 Alchoarismi、Algorismi,最终变成了 algorithm” [@bellos_alex_2011]。在计算时代,算法指的是解决问题的一系列步骤,从而得出预定义的输出。输入必须在适当的数据结构中正式定义[@wise_gis_2001]。算法通常从流程图或伪代码开始,展示过程的目标,然后在代码中实现。为了简化可用性,常见的算法通常被封装在函数内,这些函数可能会隐藏一些或所有已采取的步骤(除非你查看函数的源代码,参见函数节)。

地理算法,比如我们在GIS章节中遇到的,是接收地理数据并通常返回地理结果的算法(同样的东西还有其他术语,比如GIS算法几何算法)。这可能听起来简单,但这是一个深刻的主题,有一个专门的学术领域,计算几何,致力于它们的研究[@berg_computational_2008],以及关于该主题的大量书籍。@orourke_computational_1998,例如,使用可复制和免费提供的C代码,以一系列逐渐复杂的几何算法介绍该主题。

几何算法的一个例子是找出多边形的质心的算法。有许多方法来计算质心,其中一些仅适用于特定类型的空间数据。为了本节的目的,我们选择一种容易可视化的方法:将多边形分解成许多三角形,并找出每个三角形的质心,这种方法由 @kaiser_algorithms_1993 讨论,还简短地在 @orourke_computational_1998 中提到。在编写任何代码之前,进一步将这种方法分解成离散任务会有所帮助(后来称为步骤1到步骤4,这些也可以以示意图或伪代码的形式呈现):

  1. 将多边形分成相邻的三角形。
  2. 找出每个三角形的质心。
  3. 找出每个三角形的面积。
  4. 找出三角形质心的面积加权平均值。

这些步骤可能听起来很简单,但将单词转换成工作代码需要一些工作和大量的反复试验,即使输入有限制:算法仅适用于凸多边形,它们不包含大于180°的内角,不允许星形(decidosfdct软件包可以使用外部库对非凸多边形进行三角剖分,如algorithm 小册子中所示,该小册子托管在 geocompx.org 上)。

表示多边形最简单的数据结构是一个矩阵,其中x和y坐标在每行表示一个顶点,以追踪多边形边界的顺序,其中第一行和最后一行是相同的[@wise_gis_2001]。在这种情况下,我们将用基础的R创建一个有五个顶点的多边形,该多边形建立在GIS Algorithms [@xiao_gis_2016参见github.com/gisalgs 以获取 Python 代码]的一个示例上,如下图所示。

1
2
3
4
# generate a simple matrix representation of a polygon:
x_coords = c(10, 20, 12, 0, 0, 10)
y_coords = c(0, 15, 20, 10, 0, 0)
poly_mat = cbind(x_coords, y_coords)

现在我们有了一个示例数据集,我们准备进行上面概述的第1步。下面的代码展示了通过创建一个单一的三角形(T1)来实现这一点,该代码演示了该方法;它还通过基于公式$1/3(a + b + c)$ 来计算其质心,从而演示了第2步,其中$a$到$c$是表示三角形顶点的坐标:

1
2
3
4
5
# create a point representing the origin:
Origin = poly_mat[1, ]
# create 'triangle matrix':
T1 = rbind(Origin, poly_mat[2:3, ], Origin)
C1 = (T1[1,] + T1[2,] + T1[3,]) / 3


Illustration of polygon centroid calculation problem.

第3步是找出每个三角形的面积,以便计算一个加权平均值,该值会考虑到大三角形的不成比例影响。计算三角形面积的公式如下[@kaiser_algorithms_1993]:

$$
\frac{A_x ( B_y − C_y ) + B_x ( C_y − A_y ) + C_x ( A_y − B_y )}{ 2 }
$$

式中,$A$到$C$是三角形的三个点,而$x$和$y$分别指代 x 和 y 的维度。将这个公式翻译成能够处理矩阵表示形式的三角形 T1 数据的 R 代码如下(函数 abs() 确保了结果为正):

1
2
3
4
5
# calculate the area of the triangle represented by matrix T1:
abs(T1[1, 1] * (T1[2, 2] - T1[3, 2]) +
T1[2, 1] * (T1[3, 2] - T1[1, 2]) +
T1[3, 1] * (T1[1, 2] - T1[2, 2])) / 2
#> [1] 85

该代码块输出了正确的结果。^[可以用以下公式(该公式假设底边是水平的)来验证结果:面积是底边宽度与高度乘积的一半,$A = B * H / 2$。在这种情况下 $10 * 10 / 2 = 50$。]问题在于代码比较笨拙,如果我们想要在另一个三角形矩阵上运行它,就必须重新键入。为了使代码更具通用性,我们将在函数节中看到如何将其转换为一个函数。

第4步要求在所有三角形上(如上面所示),而不仅仅是一个三角形上,进行第2步和第3步。这就需要迭代以创建表示多边形的所有三角形,如下图所示。这里使用 lapply()vapply()来迭代每个三角形,因为它们在基础 R 中提供了一个简洁的解决方案:^[有关文档,请参见 ?lapply,更多关于迭代的信息,请参见第@ref(location)章。]

1
2
3
4
5
6
7
8
9
10
11
12
13
i = 2:(nrow(poly_mat) - 2)
T_all = lapply(i, function(x) {
rbind(Origin, poly_mat[x:(x + 1), ], Origin)
})

C_list = lapply(T_all, function(x) (x[1, ] + x[2, ] + x[3, ]) / 3)
C = do.call(rbind, C_list)

A = vapply(T_all, function(x) {
abs(x[1, 1] * (x[2, 2] - x[3, 2]) +
x[2, 1] * (x[3, 2] - x[1, 2]) +
x[3, 1] * (x[1, 2] - x[2, 2]) ) / 2
}, FUN.VALUE = double(1))


Illustration of iterative centroid algorithm with triangles. The X represents the area-weighted centroid in iterations 2 and 3.

现在,我们有条件完成第4步,使用sum(A)来计算总面积,以及使用 weighted.mean(C[, 1], A)weighted.mean(C[, 2], A) 来计算多边形的质心坐标(给警觉的读者一个练习:验证这些命令是否有效)。为了展示算法与脚本之间的联系,本节的内容已经被压缩成 11-centroid-alg.R。我们在脚本节末尾看到,这个脚本如何计算一个正方形的质心。编写脚本 的优点是,它适用于新的 poly_mat 对象(参见下面的练习,以 st_centroid() 为参考来验证这些结果):

1
2
3
source("code/11-centroid-alg.R")
#> [1] "The area is: 245"
#> [1] "The coordinates of the centroid are: 8.83, 9.22"

上面的示例表明,使用基础 R 从基础原理出发,确实可以开发出低级地理操作。它还表明,如果已经存在一个经过验证的解决方案,那么重新发明轮子可能是不值得的:如果我们的目标仅仅是找到多边形的质心\index{centroid},那么将poly_mat表示为一个sf对象并使用现有的sf::st_centroid() 函数可能会更快。然而,从1^st^ 原理编写算法的巨大好处是,你将理解整个过程的每一个步骤,这是使用其他人代码时无法保证的。另一个需要考虑的因素是性能,与低级语言如C++相比,R在进行数字计算方面可能较慢(参见地理计算软件节),并且难以优化。如果目的是开发新方法,那么计算效率不应该是优先考虑的。这一点体现在“过早优化是编程中一切(或至少大部分)邪恶的根源”[@knuth_computer_1974]这一说法中。

算法开发是困难的。这一点应该从开发一个仅仅是一种相对低效的解决方案、对实际问题(实际中凸多边形并不常见)具有有限应用的基础 R 的质心算法中显而易见。这种经验应该会让人更加重视像GEO(它是 sf::st_centroid() 的基础)和CGAL(计算几何算法库)这样的低级地理库,这些库不仅运行快,还适用于多种类型的输入几何。
这些库开源的一个重要优点是,其源代码便于学习、理解,并且(对于具备技能和信心的人)进行修改。^[事实上,CGAL函数 CGAL::centroid() 是由7个子函数组成的,如https://doc.cgal.org/latest/Kernel_23/group__centroid__grp.html 所描述,使其能够适用于多种类型的输入数据,而我们创建的解决方案仅适用于一种非常特定的输入数据类型。GEOS 函数 Centroid::getCentroid() 的底层源代码可以在 https://github.com/libgeos/geos/search?q=getCentroid 上找到。]

函数

与算法类似,函数接收一个输入并返回一个输出。然而,函数是特定编程语言中实现的,而不是“配方”本身。在 R 中,函数本身就是对象,可以以模块化的方式创建和组合。例如,我们可以创建一个执行我们质心生成算法第二步的函数,如下所示:

1
2
3
t_centroid = function(x) {
(x[1, ] + x[2, ] + x[3, ]) / 3
}

上面的示例演示了函数的两个关键组件:1)函数 主体,即定义函数如何处理输入的大括号内的代码;和 2) 形式参数,即函数使用的参数列表——在这种情况下是 x(第三个关键组件,环境,超出了本节的范围)。默认情况下,函数返回最后一个被计算的对象(在 t_centroid() 的情况下是质心的坐标)。^[你也可以通过在函数主体中添加 return(output) 明确设置函数的输出,其中 output 是要返回的结果。]

该函数现在适用于你传递给它的任何输入,如下面的命令所示,该命令计算了上一节中示例多边形中第1个三角形的面积:

1
2
3
t_centroid(T1)
#> x_coords y_coords
#> 14.0 11.7

我们还可以创建一个函数\index{function}来计算三角形的面积,我们将其命名为 t_area()

1
2
3
4
5
6
7
t_area = function(x) {
abs(
x[1, 1] * (x[2, 2] - x[3, 2]) +
x[2, 1] * (x[3, 2] - x[1, 2]) +
x[3, 1] * (x[1, 2] - x[2, 2])
) / 2
}

注意,在创建函数之后,可以用一行代码计算三角形的面积,避免了冗长代码的重复:函数是一种 泛化 代码的机制。新创建的函数t_area()接受任何对象x,假设其具有与我们一直在使用的“三角形矩阵”数据结构相同的维度,并返回其面积,如下面在T1上的示例:

1
2
t_area(T1)
#> [1] 85

我们可以通过使用它来找到一个新的三角形矩阵的面积来测试函数的泛化性,该三角形矩阵的高度为1,底边为3:

1
2
3
4
5
t_new = cbind(x = c(0, 3, 3, 0),
y = c(0, 0, 1, 0))
t_area(t_new)
#> x
#> 1.5

函数的一个有用特性是它们是模块化的。只要你知道输出会是什么,一个函数就可以用作另一个函数的构建块。因此,t_centroid()t_area()可以用作更大的函数的子组件,以执行脚本11-centroid-alg.R的工作:计算任何凸多边形的面积。下面的代码块创建了 poly_centroid() 函数,以模仿针对凸多边形的 sf::st_centroid() 的行为:^[注意,我们创建的函数在 lapply()vapply() 函数调用中被迭代地调用。]

1
2
3
4
5
6
7
8
9
poly_centroid = function(poly_mat) {
Origin = poly_mat[1, ] # create a point representing the origin
i = 2:(nrow(poly_mat) - 2)
T_all = lapply(i, function(x) {rbind(Origin, poly_mat[x:(x + 1), ], Origin)})
C_list = lapply(T_all, t_centroid)
C = do.call(rbind, C_list)
A = vapply(T_all, t_area, FUN.VALUE = double(1))
c(weighted.mean(C[, 1], A), weighted.mean(C[, 2], A))
}
1
2
poly_centroid(poly_mat)
#> [1] 8.83 9.22

poly_centroid() 这样的函数可以进一步扩展以提供不同类型的输出。例如,要将结果作为类 sfg 的对象返回,可以使用 ‘包装器’ 函数来修改 poly_centroid() 的输出,然后返回结果:

1
2
3
4
poly_centroid_sfg = function(x) {
centroid_coords = poly_centroid(x)
sf::st_point(centroid_coords)
}

我们可以通过以下方式验证输出与 sf::st_centroid() 的输出相同:

1
2
3
poly_sfc = sf::st_polygon(list(poly_mat))
identical(poly_centroid_sfg(poly_mat), sf::st_centroid(poly_sfc))
#> [1] FALSE

编程

在本章中,我们的速度很快,从脚本到函数,再到算法这个棘手的主题。我们不仅在抽象层面上讨论了它们,还创建了针对特定问题的工作示例:

  • 介绍并演示了在’多边形矩阵’上运行的脚本11-centroid-alg.R
  • 描述了允许此脚本工作的各个步骤,被称为算法\,一个计算性的配方
  • 为了通用化这个算法,我们将其转换成模块化的函数,最终组合成上一节中的函数poly_centroid()

每一个可能看似简单。然而,熟练的编程是复杂的,涉及将每个元素——脚本、算法和函数——组合成一个系统,具有效率和风格。最终的结果应该是健壮且用户友好的工具,供其他人使用。如我们预期本书的大多数读者将是编程新手,能够遵循并复现前面几节的结果是一个重大成就。编程需要许多小时的专门学习和实践才能熟练。

面对希望以有效方式实现新算法的开发者的挑战,可以通过考虑创建一个简单但并非用于生产的函数所付出的努力量来考虑:在当前状态下,poly_centroid()在大多数(非凸)多边形上失败!这提出了一个问题:如何通用化这个函数?两个选项是(1)找到三角化非凸多边形的方法(这是本章支持的在线算法文章所涵盖的主题)和(2)探索其他不依赖于三角网格的质心算法。

一个更广泛的问题是:当已经有高性能的算法被实现并打包成诸如st_centroid()这样的函数时,是否值得编程解决方案?在这个特定情况下,简单的答案是’否’。在更广泛的背景下,考虑到学习编程的好处,答案是’视情况而定’。在编程中,很容易浪费时间尝试实现一种方法,只有到最后才发现有人已经做了艰苦的工作。你可以把这一章看作是通往几何算法编程魔法的垫脚石。然而,它也可以被看作是何时尝试编程一个通用解决方案,以及何时使用现有的更高级解决方案的一堂课。肯定会有编写新函数是最佳方案的时候,但也会有使用已经存在的函数是最佳方案的时候。

“不要重复发明轮子”一样适用于编程,甚至更适用于生活中的其他方面。项目开始时进行一点研究和思考可以帮助决定编程时间最好如何使用。以下三个原则也可以帮助最大化编写代码时的努力,无论它是一个简单的脚本还是由数百个函数组成的包:

  1. DRY(不要重复自己):尽量减少代码重复,并以更少的代码行解决特定问题。
    这一原则在《R for Data Science》[@grolemund_r_2016]的函数章节中有解释。
  2. KISS(保持简单愚蠢):这一原则建议首先尝试简单解决方案,优先于复杂解决方案,需要时使用依赖项,目标是保持脚本简洁。
    这一原则是名言“事物应尽量简单,但不能更简单”的计算类比。
  3. 模块性:如果你的代码被划分成明确定义的片段,它将更容易维护。一个函数应该只做一件事,但要做得非常好。如果你的函数变得太长,考虑将其拆分成多个小函数,每个都可以用于其他目的,支持DRY和KISS原则。

我们不能保证这一章会立即让你能够为你的工作创建完美的函数。然而,我们确信其内容将帮助你决定何时是适当的尝试时机(当没有其他现有的函数解决问题,当编程任务在你的能力范围内,当解决方案的好处可能大于开发它的时间成本)。通过使用上述原则,结合通过完成上面的实例获得的实践经验,你将建立你的脚本编写、包编写和编程技能。编程的第一步可能会很慢(下面的练习不应该匆忙完成),但长期的回报可能会很大。

练习

E1. 阅读脚本 11-centroid-alg.R,它位于书籍GitHub仓库的 code 文件夹中。

  • 它遵循了在章节1.3中介绍的哪些最佳实践?
  • 在你的电脑上通过一个IDE\index{IDE}(比如 RStudio\index{RStudio})创建这个脚本的一个版本(最好是通过逐行键入脚本,用你自己的编码风格和你自己的注释,而不是复制粘贴——这将帮助你学会如何键入脚本)。以一个方形多边形为例(例如,通过 poly_mat = cbind(x = c(0, 9, 9, 0, 0), y = c(0, 0, 9, 9, 0)) 创建),逐行执行脚本。
  • 可以对脚本进行哪些更改以使其更具可重复性?
  • 如何改进文档?

E2. 在几何算法部分,我们计算了由 poly_mat 表示的多边形的面积和地理质心分别为245和8.8, 9.2。

  • 参考脚本 11-centroid-alg.R 在你自己的电脑上重现结果(奖励:键入命令 - 尽量避免复制粘贴)。
  • 结果正确吗?通过将 poly_mat 转换为名为 poly_sfcsfc 对象(使用 st_polygon()(提示:此函数接受 list() 类对象)),然后使用 st_area()st_centroid() 来验证结果。

E3. 之前提到我们创建的算法只适用于凸包。定义凸包(见几何运算章节),并测试该算法在一个凸包的多边形上的效果。

  • 奖励1:思考为什么该方法仅适用于凸包,并注意要对算法做哪些更改才能使其适用于其他类型的多边形。
  • 奖励2:基于 11-centroid-alg.R 的内容,仅使用基础R函数编写一个算法,找出以矩阵形式表示的线串的总长度。

E4. 在函数部分,我们创建了不同版本的 poly_centroid() 函数,它们生成了 sfg 类的输出(poly_centroid_sfg())和类型稳定的 matrix 输出(poly_centroid_type_stable())。
进一步扩展该函数,创建一个版本(例如,名为 poly_centroid_sf()),该版本是类型稳定的(仅接受 sf 类的输入)并且返回 sf 对象(提示:你可能需要用命令 sf::st_coordinates(x) 将对象 x 转换为矩阵)。

  • 通过运行 poly_centroid_sf(sf::st_sf(sf::st_sfc(poly_sfc))) 来验证它的工作原理
  • 当你尝试运行 poly_centroid_sf(poly_mat) 时,你得到什么错误信息?

本文由SCY翻译自《Geocomputation with R》第十章

R 等具有交互式控制台的语言的一个特点——严格来说是一个读取-求值-打印循环(REPL)—— 是你与它们互动的方式。与其依赖于在屏幕的不同部分上指点和点击,你可以将命令键入控制台,并使用 Enter 键执行它们。使用像RStudio或VS Code这样的交互式开发环境时,一个常见且有效的工作流程是将代码键入源文件的源编辑器中,并使用像Ctrl+Enter这样的快捷方式来控制代码的交互式执行。

阅读全文 »

本文由SCY翻译自《Geocomputation with R》第九章

前提条件

  • 本章需要使用我们已经在使用的以下包:
1
2
3
4
5
library(sf)
library(terra)
library(dplyr)
library(spData)
library(spDataLarge)
  • 此外,本章还使用以下可视化包(如果您想开发交互式地图应用程序,请安装shiny):
1
2
3
4
# remotes::install_github("r-tmap/tmap@v4")
library(tmap) # for static and interactive maps
library(leaflet) # for interactive maps
library(ggplot2) # tidyverse data visualization package
  • 您还需要按照以下方式读取几个数据集,以进行栅格数据的空间操作部分的内容:
1
nz_elev = rast(system.file("raster/nz_elev.tif", package = "spDataLarge"))

引言

地理研究中令人满意和重要的一个方面是传达研究结果。制图——地图制作的艺术——是一项古老的技能,涉及沟通、细节关注和创造性元素。在R中进行静态制图非常简单,可以使用plot()函数。通过使用基本R方法,可以创建高级地图。然而,本章的重点是使用专门的制图包进行地图制作。在学习新技能时,首先在一个领域深入掌握知识,然后再进行扩展,这是合乎情理的做法。制图也不例外,因此本章深入介绍了一个包(tmap),而不是浅尝辄止地介绍多个包。

除了有趣和富有创意外,制图还具有重要的实际应用。精心制作的地图可能是传达工作结果的最佳方式,但设计不良的地图可能会留下不好的印象。常见的设计问题包括文本的位置、大小和可读性差,以及颜色的选择不慎,正如《地图杂志》的风格指南中所述。此外,糟糕的地图制作可能会妨碍结果的传达:

看起来业余的地图可能会削弱您的受众理解重要信息的能力,减弱专业数据调查的呈现效果。
数千年来,地图已被用于各种各样的目的。
历史上的例子包括3000多年前的旧巴比伦王朝的建筑和土地所有权地图,以及近2000年前托勒密在他的杰作地理学中的世界地图。

制图活动在历史上曾经只由精英或代表精英进行。随着开源地图制作软件的出现,如R包tmap和QGIS中的’print composer’,任何人都可以制作高质量的地图,从而实现了’公民科学’。地图也常常是以易于理解的方式呈现地理计算研究结果的最佳途径。制图因此是地理计算的关键部分,它不仅强调描述世界,还强调改变世界。

本章介绍了如何制作各种类型的地图。接下来的部分将涵盖静态地图的各种情况,包括美学考虑、分面和插图地图。部分章节将介绍动态和交互式地图(包括网络地图和地图应用程序)。最
后,将介绍一系列备选的地图制作包,包括ggplot2cartogram

静态地图

静态地图是地理计算的最常见的可视输出类型。它们通常以标准格式保存,包括 .png.pdf用于图形光栅和矢量输出。最初,静态地图是R唯一能够生成的地图类型。随着sp的发布[见@pebesma{.email}_classes_2005],自那时以来,许多地图制作技术、函数和软件包得到了发展。然而,尽管有交互式地图的创新,静态绘图仍然是十年后R中地理数据可视化的重点。

通用的 plot() 函数是从矢量和栅格空间对象创建静态地图的最快方法。有时,在项目开发阶段,简单性和速度是首要考虑的因素,这就是 plot() 函数的优势所在。
基本的 R 方法也是可扩展的,plot() 函数提供了许多参数选项。另一种方法是使用 grid 软件包,它允许对静态地图进行低级别的控制,如 @murrell_r_2016 的第 14 章所示。本书的这部分将重点介绍 tmap,并强调其基本的美学和布局选项。

tmap是一个功能强大且灵活的制图软件包,具有合理的默认设置。它具有简洁的语法,允许使用最少的代码创建具有吸引力的地图,这对于 ggplot2 用户来说会非常熟悉。它还具有独特的功能,通过tmap_mode()可以生成静态和交互式地图,使用相同的代码。最后,它接受比其他替代方案(如 ggplot2)更广泛的空间类别(包括 sfterra 对象)。

tmap 基础

ggplot2类似,tmap基于"图形语法"的理念。这涉及将输入数据与美学(数据如何可视化)分离:每个输入数据集可以通过多种不同的方式进行"映射",包括在地图上的位置(由数据的geometry定义)、颜色和其他视觉变量。基本构建块是 tm_shape()(定义输入数据:矢量或栅格对象),然后是一个或多个图层元素,如 tm_fill()tm_dots()。以下代码块演示了这种分层结构,生成了图中呈现的地图:

1
2
3
4
5
6
7
8
9
10
# Add fill layer to nz shape
tm_shape(nz) +
tm_fill()
# Add border layer to nz shape
tm_shape(nz) +
tm_borders()
# Add fill and border layers to nz shape
tm_shape(nz) +
tm_fill() +
tm_borders()


New Zealand’s shape plotted with fill (left), border (middle) and fill and border (right) layers added using tmap functions.

在这种情况下,传递给tm_shape()的对象是nz,一个代表新西兰各个地区的sf对象。添加图层以在视觉上表示nz,使用tm_fill()tm_borders()分别在上图的左侧面板和中间面板中创建了阴影区。

  • tm_fill(): (多)多边形的阴影区域
  • tm_borders(): (多)多边形的边界轮廓
  • tm_polygons(): (多)多边形的阴影区域和边界轮廓
  • tm_lines(): (多)线串的线条
  • tm_symbols(): (多)点、线串和多边形的符号
  • tm_raster(): 栅格数据的彩色单元格(也有用于具有三个图层的栅格的 tm_rgb()
  • tm_text(): (多)点、线串和多边形的文本信息
1
<!-- 请参阅 `help("tmap-element")` 获取所有可能的图层类型的完整列表。这种图层叠加效果在图的右侧面板中得以体现,这是在填充图层之上添加了边界。 -->
1
2
3
4
5
6
<!--
> `qtm()` 是一个方便的函数,用于创建快速、主题、地图(因此取了一个简洁的名字)。
它非常简洁,对许多情况下都提供了很好的默认可视化效果:例如,`qtm(nz)` 等同于 `tm_shape(nz) + tm_fill() + tm_borders()`。
此外,可以使用多个 `qtm()` 调用简洁地添加图层,例如 `qtm(nz) + qtm(nz_height)`。
缺点是它使得单个图层的美学更难以控制,这就解释了为什么我们在本章中避免教授它。
-->

地图对象

tmap 的一个有用特性是它能够存储代表地图的对象。下面的代码块演示了这一点,通过将上图中的最后一个绘图保存为一个tmap类型的对象(注意使用了tm_polygons(),它将tm_fill()+tm_borders()结合成一个函数):

1
2
3
map_nz = tm_shape(nz) + tm_polygons()
class(map_nz)
#> [1] "tmap"

map_nz可以进行绘图,例如添加额外的图层(如下所示),或者只需在控制台中运行 map_nz,这相当于 print(map_nz)

可以使用 + tm_shape(new_obj) 添加新的shapes。在这种情况下,new_obj表示要绘制在之前图层之上的新的空间对象。当以这种方式添加新的形状shape时,所有后续的美学函数都会与它相关联,直到添加另一个新的shape为止。这种语法允许创建具有多个形状和图层的地图,如下面的代码块所示,使用函数 tm_raster()绘制一个栅格图层(设置col_alpha 使图层半透明):

1
2
map_nz1 = map_nz +
tm_shape(nz_elev) + tm_raster(col_alpha = 0.7)

Building on the previously created map_nz object, the preceding code creates a new map object map_nz1 that contains another shape (nz_elev) representing average elevation across New Zealand (see Figure @ref(fig:tmlayers), left).
More shapes and layers can be added, as illustrated in the code chunk below which creates nz_water, representing New Zealand’s territorial waters, and adds the resulting lines to an existing map object.

1
2
3
4
5
nz_water = st_union(nz) |>
st_buffer(22200) |>
st_cast(to = "LINESTRING")
map_nz2 = map_nz1 +
tm_shape(nz_water) + tm_lines()

There is no limit to the number of layers or shapes that can be added to tmap objects, and the same shape can even be used multiple times.
The final map illustrated in Figure @ref(fig:tmlayers) is created by adding a layer representing high points (stored in the object nz_height) onto the previously created map_nz2 object with tm_symbols() (see ?tm_symbols for details on tmap’s point plotting functions).
The resulting map, which has four layers, is illustrated in the right-hand panel of Figure @ref(fig:tmlayers):

1
2
map_nz3 = map_nz2 +
tm_shape(nz_height) + tm_symbols()

A useful and little known feature of tmap is that multiple map objects can be arranged in a single ‘metaplot’ with tmap_arrange().
This is demonstrated in the code chunk below which plots map_nz1 to map_nz3, resulting in Figure @ref(fig:tmlayers).

1
tmap_arrange(map_nz1, map_nz2, map_nz3)
Maps with additional layers added to the final map of Figure 9.1.

(\#fig:tmlayers)Maps with additional layers added to the final map of Figure 9.1.

More elements can also be added with the + operator.
Aesthetic settings, however, are controlled by arguments to layer functions.

可视化变量

The plots in the previous section demonstrate tmap’s default aesthetic settings.
Gray shades are used for tm_fill() and tm_symbols() layers and a continuous black line is used to represent lines created with tm_lines().
Of course, these default values and other aesthetics can be overridden.
The purpose of this section is to show how.

There are two main types of map aesthetics: those that change with the data and those that are constant.
Unlike ggplot2, which uses the helper function aes() to represent variable aesthetics, tmap accepts a few aesthetic arguments, depending on a selected layer type:

  • fill: fill color of a polygon
  • col: color of a polygon border, line, point, or raster
  • lwd: line width
  • lty: line type
  • size: size of a symbol
  • shape: shape of a symbol

Additionally, we may customize the fill and border color transparency using fill_alpha and col_alpha.

To map a variable to an aesthetic, pass its column name to the corresponding argument, and to set a fixed aesthetic, pass the desired value instead.[^1]
The impact of setting these with fixed values is illustrated in Figure @ref(fig:tmstatic).

[^1]: If there is a clash between a fixed value and a column name, the column name takes precedence.
This can be verified by running the next code chunk after running nz$red = 1:nrow(nz).

1
2
3
4
5
6
7
8
ma1 = tm_shape(nz) + tm_polygons(fill = "red")
ma2 = tm_shape(nz) + tm_polygons(fill = "red", fill_alpha = 0.3)
ma3 = tm_shape(nz) + tm_polygons(col = "blue")
ma4 = tm_shape(nz) + tm_polygons(lwd = 3)
ma5 = tm_shape(nz) + tm_polygons(lty = 2)
ma6 = tm_shape(nz) + tm_polygons(fill = "red", fill_alpha = 0.3,
col = "blue", lwd = 3, lty = 2)
tmap_arrange(ma1, ma2, ma3, ma4, ma5, ma6)
The impact of changing commonly used fill and border aesthetics to fixed values.

(\#fig:tmstatic)The impact of changing commonly used fill and border aesthetics to fixed values.

Like base R plots, arguments defining aesthetics can also receive values that vary.
Unlike the base R code below (which generates the left panel in Figure @ref(fig:tmcol)), tmap aesthetic arguments will not accept a numeric vector:

1
2
3
plot(st_geometry(nz), col = nz$Land_area)  # works
tm_shape(nz) + tm_fill(col = nz$Land_area) # fails
#> Error: palette should be a character value

Instead col (and other aesthetics that can vary such as lwd for line layers and size for point layers) requires a character string naming an attribute associated with the geometry to be plotted.
Thus, one would achieve the desired result as follows (plotted in the right-hand panel of Figure @ref(fig:tmcol)):

1
tm_shape(nz) + tm_fill(fill = "Land_area")
Comparison of base (left) and tmap (right) handling of a numeric color field.Comparison of base (left) and tmap (right) handling of a numeric color field.

(\#fig:tmcol)Comparison of base (left) and tmap (right) handling of a numeric color field.

Each visual variable has three related additional arguments, with prefixes of .scale, .legend, and .free.
For example, the tm_fill() function has arguments such as fill, fill.scale, fill.legend, and fill.free.
The .scale argument determines how the provided values are represented on the map and in the legend (Section @ref(scales)), while the .legend argument is used to customize the legend settings, such as its title, orientation, or position (Section @ref(legends)).
The .free argument is relevant only for maps with many facets to determine if each facet has the same or different scale and legend (Section @ref(faceted-maps)).

标度

\index{tmap (package)!scales} Scales control how the values are represented on the map and in the legend, and largely depend on the selected visual variable.
For example, when our visual variable is col, then col.scale controls how the colors of spatial objects are related to the provided values; and when our visual variable is size, then size.scale controls how the sizes represent the provided values.
By default, the used scale is tm_scale(), which selects the visual settings automatically given by the data type (factor, numeric, and integer).

\index{tmap (package)!color breaks} Let’s see how the scales work by customizing polygons’ fill colors.
Color settings are an important part of map design – they can have a major impact on how spatial variability is portrayed as illustrated in Figure @ref(fig:tmpal).
This figure shows four ways of coloring regions in New Zealand depending on median income, from left to right (and demonstrated in the code chunk below):

  • The default setting uses ‘pretty’ breaks, described in the next paragraph
  • breaks allows you to manually set the breaks
  • n sets the number of bins into which numeric variables are categorized
  • values defines the color scheme, for example, BuGn
1
2
3
4
5
6
7
tm_shape(nz) + tm_polygons(fill = "Median_income")
tm_shape(nz) + tm_polygons(fill = "Median_income",
fill.scale = tm_scale(breaks = c(0, 30000, 40000, 50000)))
tm_shape(nz) + tm_polygons(fill = "Median_income",
fill.scale = tm_scale(n = 10))
tm_shape(nz) + tm_polygons(fill = "Median_income",
fill.scale = tm_scale(values = "BuGn"))
Illustration of settings that affect color settings. The results show (from left to right): default settings, manual breaks, n breaks, and the impact of changing the palette.

(\#fig:tmpal)Illustration of settings that affect color settings. The results show (from left to right): default settings, manual breaks, n breaks, and the impact of changing the palette.

\BeginKnitrBlock{rmdnote}

All of the above arguments (breaks, n, and values) also work for other types of visual variables.
For example, values expects a vector of colors or a palette name for fill.scale or col.scale, a vector of sizes for size.scale, or a vector of symbols for shape.scale.
\EndKnitrBlock{rmdnote}

\index{tmap (package)!break styles} We are also able to customize scales using a family of functions that start with the tm_scale_ prefix.
The most important ones are tm_scale_intervals(), tm_scale_continuous(), and tm_scale_categorical().

\index{tmap (package)!interval scale} The tm_scale_intervals() function splits the input data values into a set of intervals.
In addition to manually setting breaks, tmap allows users to specify algorithms to create breaks with the style argument automatically.
Here are some of the most useful scale functions (Figure @ref(fig:break-styles)):

  • style = "pretty": the default setting, rounds breaks into whole numbers where possible and spaces them evenly
  • style = "equal": divides input values into bins of equal range and is appropriate for variables with a uniform distribution (not recommended for variables with a skewed distribution as the resulting map may end-up having little color diversity)
  • style = "quantile": ensures the same number of observations fall into each category (with the potential downside that bin ranges can vary widely)
  • style = "jenks": identifies groups of similar values in the data and maximizes the differences between categories
  • style = "log10_pretty": a common logarithmic (the logarithm to base 10) version of the regular pretty style used for variables with a right-skewed distribution

\BeginKnitrBlock{rmdnote}

Although style is an argument of tmap functions, in fact it originates as an argument in classInt::classIntervals() — see the help page of this function for details.
\EndKnitrBlock{rmdnote}

Illustration of different interval scales' methods set using the style argument in tmap.

(\#fig:break-styles)Illustration of different interval scales' methods set using the style argument in tmap.

\index{tmap (package)!continuous scale} The tm_scale_continuous() function present a large number of colors over continuous color fields and are particularly suited for continuous rasters.
In case of variables with skewed distribution you can also use its variants – tm_scale_continuous_log() and tm_scale_continuous_log1p().
\index{tmap (package)!categorical scale} Finally, tm_scale_categorical() was designed to represent categorical values and assures that each category receives a unique color (Figure @ref(fig:concat)).

1
2
3
4
5
6
7
8
9
10
#> Warning in strwidth(comp$text, units = "inch", family = comp$fontfamily, : no
#> font could be found for family "monospace"

#> Warning in strwidth(comp$text, units = "inch", family = comp$fontfamily, : no
#> font could be found for family "monospace"
#> Warning in strwidth(comp$text, units = "inch", family = comp$fontfamily, : font
#> family 'monospace' not found, will use 'sans' instead

#> Warning in strwidth(comp$text, units = "inch", family = comp$fontfamily, : font
#> family 'monospace' not found, will use 'sans' instead
Illustration of continuous and categorical scales in tmap.

(\#fig:concat)Illustration of continuous and categorical scales in tmap.

\index{tmap (package)!palettes} Palettes define the color ranges associated with the bins and determined by the tm_scale_*() functions, and its breaks and n arguments described above.
The default color palette is specified in tm_layout() (see Section @ref(layouts) to learn more); however, it could be quickly changed using the values argument.
It expects a vector of colors or a new color palette name, which can be find interactively with cols4all::c4a_gui().
You can also add a - as the color palette name prefix to reverse the palette order.

\BeginKnitrBlock{rmdnote}

All of the default values of the visual variables, such as default color palettes for different types of input variables, can be found with tmap_options()$values.var.
\EndKnitrBlock{rmdnote}

There are three main groups of color palettes\index{map making!color palettes}: categorical, sequential and diverging (Figure @ref(fig:colpal)), and each of them serves a different purpose.[^2]
Categorical palettes consist of easily distinguishable colors and are most appropriate for categorical data without any particular order such as state names or land cover classes.
Colors should be intuitive: rivers should be blue, for example, and pastures green.
Avoid too many categories: maps with large legends and many colors can be uninterpretable.

[^2]: A fourth group of color palettes, called bivariate, also exists.
They are used when we want to represent relations between two variables on one map.

The second group is sequential palettes.
These follow a gradient, for example from light to dark colors (light colors often tend to represent lower values), and are appropriate for continuous (numeric) variables.
Sequential palettes can be single (greens goes from light to dark blue, for example) or multi-color/hue (yl_gn_bu is gradient from light yellow to blue via green, for example), as demonstrated in the code chunk below — output not shown, run the code yourself to see the results!

1
2
tm_shape(nz) + tm_polygons("Median_income", fill.scale = tm_scale(values = "greens"))
tm_shape(nz) + tm_polygons("Median_income", fill.scale = tm_scale(values = "yl_gn_bu"))

The third group, diverging palettes, typically range between three distinct colors (purple-white-green in Figure @ref(fig:colpal)) and are usually created by joining two single-color sequential palettes with the darker colors at each end.
Their main purpose is to visualize the difference from an important reference point, e.g., a certain temperature, the median household income or the mean probability for a drought event.
The reference point’s value can be adjusted in tmap using the midpoint argument.

1
2
3
tm_shape(nz) + 
tm_polygons("Median_income",
fill.scale = tm_scale_continuous(values = "pu_gn_div", midpoint = 28000))
Examples of categorical, sequential and diverging palettes.

(\#fig:colpal)Examples of categorical, sequential and diverging palettes.

There are two important principles for consideration when working with colors: perceptibility and accessibility.
Firstly, colors on maps should match our perception.
This means that certain colors are viewed through our experience and also cultural lenses.
For example, green colors usually represent vegetation or lowlands and blue is connected with water or cool.
Color palettes should also be easy to understand to effectively convey information.
It should be clear which values are lower and which are higher, and colors should change gradually.

Secondly, changes in colors should be accessible to the largest number of people.

Therefore, it is important to use colorblind friendly palettes as often as possible.[^3]

[^3]: See the “Color vision” options and the “Color Blind Friendliness” panel in cols4all::c4a_gui().

图例

\index{tmap (package)!legends} After we decided on our visual variable and its properties, we should move our attention toward the related map legend style.
Using the tm_legend() function, we may change its title, position, orientation, or even disable it.
The most important argument in this function is title, which sets the title of the associated legend.
In general, a map legend title should provide two pieces of information: what the legend represents and what are the units of the presented variable.
The following code chunk demonstrates this functionality by providing a more attractive name than the variable name Land_area (note the use of expression() to create superscript text):

1
2
3
legend_title = expression("Area (km"^2*")")
map_nza = tm_shape(nz) +
tm_polygons(fill = "Land_area", fill.legend = tm_legend(title = legend_title))

The default legend orientation in tmap is "portrait", however, an alternative legend orientation, "landscape", is also possible.
Other than that, we can also customize the location of the legend using the position argument.

1
2
3
4
map_nza2 = tm_shape(nz) +
tm_polygons(fill = "Land_area", fill.legend = tm_legend(title = legend_title,
orientation = "landscape",
position = tm_pos_out("center", "bottom")))

The legend position (and also the position of several other map elements in tmap) can be customized using one of a few functions.
The two most important are:

  • tm_pos_out(): the default, adds the legend outside of the map frame area. We can customize its location with two values that represent the horizontal position ("left", "center", or "right"), and the vertical position ("bottom", "center", or "top")
  • tm_pos_in(): puts the legend inside of the map frame area. We may decided on its position using two arguments, where the first one can be "left", "center", or "right", and the second one can be "bottom", "center", or "top".

Alternatively, we may just provide a vector of two values (or two numbers between 0 and 1) here – and in such case, the legend will be put inside the map frame.

布局

\index{tmap (package)!layouts} The map layout refers to the combination of all map elements into a cohesive map.
Map elements include among others the objects to be mapped, the title, the scale bar, the map grid, and margins, while the color settings covered in the previous section relate to the palette and break-points used to affect how the map looks.
Both may result in subtle changes that can have an equally large impact on the impression left by your maps.

Additional map elements such as graticules \index{tmap (package)!graticules}, north arrows\index{tmap (package)!north arrows}, scale bars\index{tmap (package)!scale bars} and map titles have their own functions: tm_graticules(), tm_compass(), tm_scalebar(), and tm_title() (Figure @ref(fig:na-sb)).[^4]

[^4]: Another additional map elements include tm_grid(), tm_logo() and tm_credits().

1
2
3
4
5
map_nz + 
tm_graticules() +
tm_compass(type = "8star", position = c("left", "top")) +
tm_scalebar(breaks = c(0, 100, 200), text.size = 1, position = c("left", "top")) +
tm_title("New Zealand")
Map with additional elements - a north arrow and scale bar.

(\#fig:na-sb)Map with additional elements - a north arrow and scale bar.

tmap also allows a wide variety of layout settings to be changed, some of which, produced using the following code (see args(tm_layout) or ?tm_layout for a full list), are illustrated in Figure @ref(fig:layout1):

1
2
3
map_nz + tm_layout(scale = 4)
map_nz + tm_layout(bg.color = "lightblue")
map_nz + tm_layout(frame = FALSE)
Layout options specified by (from left to right) title, scale, bg.color and frame arguments.

(\#fig:layout1)Layout options specified by (from left to right) title, scale, bg.color and frame arguments.

The other arguments in tm_layout() provide control over many more aspects of the map in relation to the canvas on which it is placed.
Here are some useful layout settings (some of which are illustrated in Figure @ref(fig:layout2)):

  • Margin settings including outer.margin and inner.margin
  • Font settings controlled by fontface and fontfamily
  • Legend settings including options such as legend.show (whether or not to show the legend) legend.orientation, legend.position, and legend.frame
  • Frame width (frame.lwd) and an option to allow double lines (frame.double.line)
  • Color settings controlling color.sepia.intensity (how yellowy the map looks) and color.saturation (a color-grayscale)
Illustration of selected layout options.

(\#fig:layout2)Illustration of selected layout options.

地图分面

\index{map making!faceted maps} \index{tmap (package)!faceted maps} Faceted maps, also referred to as ‘small multiples’, are composed of many maps arranged side-by-side, and sometimes stacked vertically.
Facets enable the visualization of how spatial relationships change with respect to another variable, such as time.
The changing populations of settlements, for example, can be represented in a faceted map with each panel representing the population at a particular moment in time.
The time dimension could be represented via another visual variable such as color.
However, this risks cluttering the map because it will involve multiple overlapping points (cities do not tend to move over time!).

Typically all individual facets in a faceted map contain the same geometry data repeated multiple times, once for each column in the attribute data (this is the default plotting method for sf objects, see Chapter @ref(spatial-class)).
However, facets can also represent shifting geometries such as the evolution of a point pattern over time.
This use case of faceted plot is illustrated in Figure @ref(fig:urban-facet).

1
2
3
4
5
6
7
8
urb_1970_2030 = urban_agglomerations |> 
filter(year %in% c(1970, 1990, 2010, 2030))

tm_shape(world) +
tm_polygons() +
tm_shape(urb_1970_2030) +
tm_symbols(fill = "black", col = "white", size = "population_millions") +
tm_facets_wrap(by = "year", nrow = 2)
Faceted map showing the top 30 largest urban agglomerations from 1970 to 2030 based on population projections by the United Nations.

(\#fig:urban-facet)Faceted map showing the top 30 largest urban agglomerations from 1970 to 2030 based on population projections by the United Nations.

The preceding code chunk demonstrates key features of faceted maps created using the tm_facets_wrap() function:

  • Shapes that do not have a facet variable are repeated (the countries in world in this case)
  • The by argument which varies depending on a variable ("year" in this case)
  • The nrow/ncol setting specifying the number of rows and columns that facets should be arranged into

Alternatively, it is possible to use the tm_facets_grid() function that allows to have facets based on up to three different variables: one for rows, one for columns, and possibly one for pages.

In addition to their utility for showing changing spatial relationships, faceted maps are also useful as the foundation for animated maps (see Section @ref(animated-maps)).

插图地图

\index{map making!inset maps} \index{tmap (package)!inset maps} An inset map is a smaller map rendered within or next to the main map.
It could serve many different purposes, including providing a context (Figure @ref(fig:insetmap1)) or bringing some non-contiguous regions closer to ease their comparison (Figure @ref(fig:insetmap2)).
They could be also used to focus on a smaller area in more detail or to cover the same area as the map, but representing a different topic.

In the example below, we create a map of the central part of New Zealand’s Southern Alps.
Our inset map will show where the main map is in relation to the whole New Zealand.
The first step is to define the area of interest, which can be done by creating a new spatial object, nz_region.

1
2
3
4
nz_region = st_bbox(c(xmin = 1340000, xmax = 1450000,
ymin = 5130000, ymax = 5210000),
crs = st_crs(nz_height)) |>
st_as_sfc()

In the second step, we create a base map showing the New Zealand’s Southern Alps area.
This is a place where the most important message is stated.

1
2
3
4
5
nz_height_map = tm_shape(nz_elev, bbox = nz_region) +
tm_raster(col.scale = tm_scale_continuous(values = "YlGn"),
col.legend = tm_legend(position = c("left", "top"))) +
tm_shape(nz_height) + tm_symbols(shape = 2, col = "red", size = 1) +
tm_scalebar(position = c("left", "bottom"))

The third step consists of the inset map creation.
It gives a context and helps to locate the area of interest.
Importantly, this map needs to clearly indicate the location of the main map, for example by stating its borders.

1
2
3
4
nz_map = tm_shape(nz) + tm_polygons() +
tm_shape(nz_height) + tm_symbols(shape = 2, col = "red", size = 0.1) +
tm_shape(nz_region) + tm_borders(lwd = 3) +
tm_layout(bg.color = "lightblue")

One of the main differences between regular charts (e.g., scatterplots) and maps is that the input data determine the aspect ratio of maps.
Thus, in this case, we need to calculate the aspect ratios of our two main datasets, nz_region and nz.
The following function, norm_dim() returns the normalized width ("w") and height ("h") of the object (as "snpc" units understood my the graphic device).

1
2
3
4
5
6
7
8
9
10
11
library(grid)
norm_dim = function(obj){
bbox = st_bbox(obj)
width = bbox[["xmax"]] - bbox[["xmin"]]
height = bbox[["ymax"]] - bbox[["ymin"]]
w = width / max(width, height)
h = height / max(width, height)
return(unit(c(w, h), "snpc"))
}
main_dim = norm_dim(nz_region)
ins_dim = norm_dim(nz)

Next, knowing the aspect ratios, we need to specify the sizes and locations of our two maps – the main map and the inset map – using the viewport() function.
A viewport is part of a graphics device we use to draw the graphical elements at a given moment.
The viewport of our main map is just the representation of its aspect ratio.

1
main_vp = viewport(width = main_dim[1], height = main_dim[2])

On the other hand, the viewport of the inset map needs to specify its size and location.
Here, we would make the inset map twice smaller as the main one by multiplying the width and height by 0.5, and we will locate it 0.5 cm from the bottom right of the main map frame.

1
2
3
ins_vp = viewport(width = ins_dim[1] * 0.5, height = ins_dim[2] * 0.5,
x = unit(1, "npc") - unit(0.5, "cm"), y = unit(0.5, "cm"),
just = c("right", "bottom"))

Finally, we combine the two maps by creating a new, blank canvas, printing out the main map, and then placing the inset map inside of the main map viewport.

1
2
3
4
grid.newpage()
print(nz_height_map, vp = main_vp)
pushViewport(main_vp)
print(nz_map, vp = ins_vp)
Inset map providing a context - location of the central part of the Southern Alps in New Zealand.

(\#fig:insetmap1)Inset map providing a context - location of the central part of the Southern Alps in New Zealand.

Inset map can be saved to file either by using a graphic device (see Section @ref(visual-outputs)) or the tmap_save() function and its arguments - insets_tm and insets_vp.

Inset maps are also used to create one map of non-contiguous areas.
Probably, the most often used example is a map of the United States, which consists of the contiguous United States, Hawaii and Alaska.
It is very important to find the best projection for each individual inset in these types of cases (see Chapter @ref(reproj-geo-data) to learn more).
We can use US National Atlas Equal Area for the map of the contiguous United States by putting its EPSG code in the projection argument of tm_shape().

1
2
us_states_map = tm_shape(us_states, projection = "EPSG:2163") + tm_polygons() + 
tm_layout(frame = FALSE)

The rest of our objects, hawaii and alaska, already have proper projections; therefore, we just need to create two separate maps:

1
2
3
4
5
6
7
8
9
hawaii_map = tm_shape(hawaii) +
tm_polygons() +
tm_title("Hawaii") +
tm_layout(frame = FALSE, bg.color = NA,
title.position = c("LEFT", "BOTTOM"))
alaska_map = tm_shape(alaska) +
tm_polygons() +
tm_title("Alaska") +
tm_layout(frame = FALSE, bg.color = NA)

The final map is created by combining and arranging these three maps:

1
2
3
us_states_map
print(hawaii_map, vp = grid::viewport(0.35, 0.1, width = 0.2, height = 0.1))
print(alaska_map, vp = grid::viewport(0.15, 0.15, width = 0.3, height = 0.3))
Map of the United States.

(\#fig:insetmap2)Map of the United States.

The code presented above is compact and can be used as the basis for other inset maps but the results, in Figure @ref(fig:insetmap2), provide a poor representation of the locations of Hawaii and Alaska.
For a more in-depth approach, see the us-map vignette from the geocompkg.

动画地图

\index{map making!animated maps} \index{tmap (package)!animated maps} Faceted maps, described in Section @ref(faceted-maps), can show how spatial distributions of variables change (e.g., over time), but the approach has disadvantages.
Facets become tiny when there are many of them.
Furthermore, the fact that each facet is physically separated on the screen or page means that subtle differences between facets can be hard to detect.

Animated maps solve these issues.
Although they depend on digital publication, this is becoming less of an issue as more and more content moves online.
Animated maps can still enhance paper reports: you can always link readers to a web-page containing an animated (or interactive) version of a printed map to help make it come alive.
There are several ways to generate animations in R, including with animation packages such as gganimate, which builds on ggplot2 (see Section @ref(other-mapping-packages)).
This section focuses on creating animated maps with tmap because its syntax will be familiar from previous sections and the flexibility of the approach.

Figure @ref(fig:urban-animated) is a simple example of an animated map.
Unlike the faceted plot, it does not squeeze multiple maps into a single screen and allows the reader to see how the spatial distribution of the world’s most populous agglomerations evolve over time (see the book’s website for the animated version).

Animated map showing the top 30 largest urban agglomerations from 1950 to 2030 based on population projects by the United Nations. Animated version available online at: r.geocompx.org.

(\#fig:urban-animated)Animated map showing the top 30 largest urban agglomerations from 1950 to 2030 based on population projects by the United Nations. Animated version available online at: r.geocompx.org.

The animated map illustrated in Figure @ref(fig:urban-animated) can be created using the same tmap techniques that generate faceted maps, demonstrated in Section @ref(faceted-maps).
There are two differences, however, related to arguments in tm_facets():

  • nrow = 1, ncol = 1 are added to keep one moment in time as one layer
  • free.coords = FALSE, which maintains the map extent for each map iteration

These additional arguments are demonstrated in the subsequent code chunk[^5]:

[^5]: There is also a shortcut for this approach: tm_facets_pagewise().

1
2
3
urb_anim = tm_shape(world) + tm_polygons() + 
tm_shape(urban_agglomerations) + tm_symbols(size = "population_millions") +
tm_facets(by = "year", nrow = 1, ncol = 1, free.coords = FALSE)

The resulting urb_anim represents a set of separate maps for each year.
The final stage is to combine them and save the result as a .gif file with tmap_animation().
The following command creates the animation illustrated in Figure @ref(fig:urban-animated), with a few elements missing, that we will add in during the exercises:

1
tmap_animation(urb_anim, filename = "urb_anim.gif", delay = 25)

Another illustration of the power of animated maps is provided in Figure @ref(fig:animus).
This shows the development of states in the United States, which first formed in the east and then incrementally to the west and finally into the interior.
Code to reproduce this map can be found in the script 09-usboundaries.R.

Animated map showing population growth, state formation and boundary changes in the United States, 1790-2010. Animated version available online at r.geocompx.org.

(\#fig:animus)Animated map showing population growth, state formation and boundary changes in the United States, 1790-2010. Animated version available online at r.geocompx.org.

交互地图

\index{map making!interactive maps} \index{tmap (package)!interactive maps} While static and animated maps can enliven geographic datasets, interactive maps can take them to a new level.
Interactivity can take many forms, the most common and useful of which is the ability to pan around and zoom into any part of a geographic dataset overlaid on a ‘web map’ to show context.
Less advanced interactivity levels include popups which appear when you click on different features, a kind of interactive label.
More advanced levels of interactivity include the ability to tilt and rotate maps, as demonstrated in the mapdeck example below, and the provision of “dynamically linked” sub-plots which automatically update when the user pans and zooms.

The most important type of interactivity, however, is the display of geographic data on interactive or ‘slippy’ web maps.
The release of the leaflet package in 2015 (that uses the leaflet JavaScript library) revolutionized interactive web map creation from within R and a number of packages have built on these foundations adding new features (e.g., leaflet.extras) and making the creation of web maps as simple as creating static maps (e.g., mapview and tmap).
This section illustrates each approach in the opposite order.
We will explore how to make slippy maps with tmap (the syntax of which we have already learned), mapview\index{mapview (package)}, mapdeck\index{mapdeck (package)} and finally leaflet\index{leaflet (package)} (which provides low-level control over interactive maps).

A unique feature of tmap mentioned in Section @ref(static-maps) is its ability to create static and interactive maps using the same code.
Maps can be viewed interactively at any point by switching to view mode, using the command tmap_mode("view").
This is demonstrated in the code below, which creates an interactive map of New Zealand based on the tmap object map_nz, created in Section @ref(map-obj), and illustrated in Figure @ref(fig:tmview):

1
2
tmap_mode("view")
map_nz

(\#fig:tmview)Interactive map of New Zealand created with tmap in view mode. Interactive version available online at: r.geocompx.org.

Now that the interactive mode has been ‘turned on’, all maps produced with tmap will launch (another way to create interactive maps is with the tmap_leaflet() function).
Notable features of this interactive mode include the ability to specify the basemap with tm_basemap() (or tmap_options()) as demonstrated below (result not shown):

1
map_nz + tm_basemap(server = "OpenTopoMap")

An impressive and little-known feature of tmap’s view mode is that it also works with faceted plots.
The argument sync in tm_facets() can be used in this case to produce multiple maps with synchronized zoom and pan settings, as illustrated in Figure @ref(fig:sync), which was produced by the following code:

1
2
3
4
world_coffee = left_join(world, coffee_data, by = "name_long")
facets = c("coffee_production_2016", "coffee_production_2017")
tm_shape(world_coffee) + tm_polygons(facets) +
tm_facets_wrap(nrow = 1, sync = TRUE)
Faceted interactive maps of global coffee production in 2016 and 2017 in sync, demonstrating tmap's view mode in action.

(\#fig:sync)Faceted interactive maps of global coffee production in 2016 and 2017 in sync, demonstrating tmap's view mode in action.

Switch tmap back to plotting mode with the same function:

1
2
tmap_mode("plot")
#> tmap mode set to 'plot'

If you are not proficient with tmap, the quickest way to create interactive maps in R may be with mapview\index{mapview (package)}.
The following ‘one liner’ is a reliable way to interactively explore a wide range of geographic data formats:

1
mapview::mapview(nz)
Illustration of mapview in action.

(\#fig:mapview)Illustration of mapview in action.

mapview has a concise syntax yet is powerful.
By default, it has some standard GIS functionality such as mouse position information, attribute queries (via pop-ups), scale bar, and zoom-to-layer buttons.
It also offers advanced controls including the ability to ‘burst’ datasets into multiple layers and the addition of multiple layers with + followed by the name of a geographic object.
Additionally, it provides automatic coloring of attributes via the zcol argument.
In essence, it can be considered a data-driven leaflet API\index{API} (see below for more information about leaflet).
Given that mapview always expects a spatial object (including sf and SpatRaster) as its first argument, it works well at the end of piped expressions.
Consider the following example where sf is used to intersect lines and polygons and then is visualized with mapview (Figure @ref(fig:mapview2)).

1
2
3
4
5
6
7
8
9
library(mapview)
oberfranken = subset(franconia, district == "Oberfranken")
trails |>
st_transform(st_crs(oberfranken)) |>
st_intersection(oberfranken) |>
st_collection_extract("LINESTRING") |>
mapview(color = "red", lwd = 3, layer.name = "trails") +
mapview(franconia, zcol = "district") +
breweries
Using mapview at the end of a sf-based pipe expression.

(\#fig:mapview2)Using mapview at the end of a sf-based pipe expression.

One important thing to keep in mind is that mapview layers are added via the + operator (similar to ggplot2 or tmap).
By default, mapview uses the leaflet JavaScript library to render the output maps, which is user-friendly and has a lot of features.
However, some alternative rendering libraries could be more performant (work more smoothly on larger datasets).
mapview allows to set alternative rendering libraries ("leafgl" and "mapdeck") in the mapviewOptions().[^6]
For further information on mapview, see the package’s website at: r-spatial.github.io/mapview/.

[^6]: You may also try to use mapviewOptions(georaster = TRUE) for more performant visualizations of large raster data.

There are other ways to create interactive maps with R.
The googleway package\index{googleway (package)}, for example, provides an interactive mapping interface that is flexible and extensible (see the googleway-vignette for details).
Another approach by the same author is mapdeck, which provides access to Uber’s Deck.gl framework\index{mapdeck (package)}.
Its use of WebGL enables it to interactively visualize large datasets up to millions of points.
The package uses Mapbox access tokens, which you must register for before using the package.

\BeginKnitrBlock{rmdnote}

Note that the following block assumes the access token is stored in your R environment as MAPBOX=your_unique_key.
This can be added with usethis::edit_r_environ().
\EndKnitrBlock{rmdnote}

A unique feature of mapdeck is its provision of interactive 2.5D perspectives, illustrated in Figure @ref(fig:mapdeck).
This means you can can pan, zoom and rotate around the maps, and view the data ‘extruded’ from the map.
Figure @ref(fig:mapdeck), generated by the following code chunk, visualizes road traffic crashes in the UK, with bar height representing casualties per area.

1
2
3
4
5
6
7
8
library(mapdeck)
set_token(Sys.getenv("MAPBOX"))
crash_data = read.csv("https://git.io/geocompr-mapdeck")
crash_data = na.omit(crash_data)
ms = mapdeck_style("dark")
mapdeck(style = ms, pitch = 45, location = c(0, 52), zoom = 4) |>
add_grid(data = crash_data, lat = "lat", lon = "lng", cell_size = 1000,
elevation_scale = 50, colour_range = hcl.colors(6, "plasma"))
Map generated by mapdeck, representing road traffic casualties across the UK. Height of 1 km cells represents number of crashes.

(\#fig:mapdeck)Map generated by mapdeck, representing road traffic casualties across the UK. Height of 1 km cells represents number of crashes.

You can zoom and drag the map in the browser, in addition to rotating and tilting it when pressing Cmd/Ctrl.
Multiple layers can be added with the pipe operator, as demonstrated in the mapdeck vignettes.
mapdeck also supports sf objects, as can be seen by replacing the add_grid() function call in the preceding code chunk with add_polygon(data = lnd, layer_id = "polygon_layer"), to add polygons representing London to an interactive tilted map.

Last but not least is leaflet which is the most mature and widely used interactive mapping package in R\index{leaflet (package)}.
leaflet provides a relatively low-level interface to the Leaflet JavaScript library and many of its arguments can be understood by reading the documentation of the original JavaScript library (see leafletjs.com).

Leaflet maps are created with leaflet(), the result of which is a leaflet map object which can be piped to other leaflet functions.
This allows multiple map layers and control settings to be added interactively, as demonstrated in the code below which generates Figure @ref(fig:leaflet) (see rstudio.github.io/leaflet/ for details).

1
2
3
4
5
6
7
8
pal = colorNumeric("RdYlBu", domain = cycle_hire$nbikes)
leaflet(data = cycle_hire) |>
addProviderTiles(providers$CartoDB.Positron) |>
addCircles(col = ~pal(nbikes), opacity = 0.9) |>
addPolygons(data = lnd, fill = FALSE) |>
addLegend(pal = pal, values = ~nbikes) |>
setView(lng = -0.1, 51.5, zoom = 12) |>
addMiniMap()
The leaflet package in action, showing cycle hire points in London. See interactive version [online](https://geocompr.github.io/img/leaflet.html).

(\#fig:leaflet)The leaflet package in action, showing cycle hire points in London. See interactive version [online](https://geocompr.github.io/img/leaflet.html).

地图应用

\index{map making!mapping applications} The interactive web maps demonstrated in Section @ref(interactive-maps) can go far.
Careful selection of layers to display, base-maps and pop-ups can be used to communicate the main results of many projects involving geocomputation.
But the web mapping approach to interactivity has limitations:

  • Although the map is interactive in terms of panning, zooming and clicking, the code is static, meaning the user interface is fixed
  • All map content is generally static in a web map, meaning that web maps cannot scale to handle large datasets easily
  • Additional layers of interactivity, such a graphs showing relationships between variables and ‘dashboards’ are difficult to create using the web-mapping approach

Overcoming these limitations involves going beyond static web mapping and towards geospatial frameworks and map servers.
Products in this field include GeoDjango\index{GeoDjango} (which extends the Django web framework and is written in Python)\index{Python}, MapServer\index{MapServer} (a framework for developing web applications, largely written in C and C++)\index{C++} and GeoServer (a mature and powerful map server written in Java\index{Java}).
Each of these is scalable, enabling maps to be served to thousands of people daily, assuming there is sufficient public interest in your maps!
The bad news is that such server-side solutions require much skilled developer time to set-up and maintain, often involving teams of people with roles such as a dedicated geospatial database administrator (DBA).

Fortunately for R programmers, web mapping applications can now be rapidly created wih shiny.\index{shiny (package)} As described in the open source book Mastering Shiny, shiny is an R package and framework for converting R code into interactive web applications.
You can embed interactive maps in shiny apps thanks to functions such as tmap::renderTmap() and leaflet::renderLeaflet().
This section gives some context, teaches the basics of shiny from a web mapping perspective and culminates in a full-screen mapping application in less than 100 lines of code.

shiny is well documented at shiny.rstudio.com, which highlights the two components of every shiny app: ‘front end’ (the bit the user sees) and ‘back end’ code.
In shiny apps, these elements are typically created in objects named ui and server within an R script named app.R, which lives in an ‘app folder’.
This allows web mapping applications to be represented in a single file, such as the CycleHireApp/app.R file in the book’s GitHub repo.

\BeginKnitrBlock{rmdnote}

In shiny apps these are often split into ui.R (short for user interface) and server.R files, naming conventions used by shiny-server, a server-side Linux application for serving shiny apps on public-facing websites.
shiny-server also serves apps defined by a single app.R file in an ‘app folder’.
Learn more at: https://github.com/rstudio/shiny-server.
\EndKnitrBlock{rmdnote}

Before considering large apps, it is worth seeing a minimal example, named ‘lifeApp’, in action.[^7]
The code below defines and launches — with the command shinyApp() — a lifeApp, which provides an interactive slider allowing users to make countries appear with progressively lower levels of life expectancy (see Figure @ref(fig:lifeApp)):

[^7]: The word ‘app’ in this context refers to ‘web application’ and should not be confused with smartphone apps, the more common meaning of the word.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
library(shiny)    # for shiny apps
library(leaflet) # renderLeaflet function
library(spData) # loads the world dataset
ui = fluidPage(
sliderInput(inputId = "life", "Life expectancy", 49, 84, value = 80),
leafletOutput(outputId = "map")
)
server = function(input, output) {
output$map = renderLeaflet({
leaflet() |>
# addProviderTiles("OpenStreetMap.BlackAndWhite") |>
addPolygons(data = world[world$lifeExp < input$life, ])})
}
shinyApp(ui, server)
Screenshot showing minimal example of a web mapping application created with shiny.

(\#fig:lifeApp)Screenshot showing minimal example of a web mapping application created with shiny.

The user interface (ui) of lifeApp is created by fluidPage().
This contains input and output ‘widgets’ — in this case, a sliderInput() (many other *Input() functions are available) and a leafletOutput().
These are arranged row-wise by default, explaining why the slider interface is placed directly above the map in Figure @ref(fig:lifeApp) (see ?column for adding content column-wise).

The server side (server) is a function with input and output arguments.
output is a list of objects containing elements generated by render*() function — renderLeaflet() which in this example generates output$map.
Input elements such as input$life referred to in the server must relate to elements that exist in the ui — defined by inputId = "life" in the code above.
The function shinyApp() combines both the ui and server elements and serves the results interactively via a new R process.
When you move the slider in the map shown in Figure @ref(fig:lifeApp), you are actually causing R code to re-run, although this is hidden from view in the user interface.

Building on this basic example and knowing where to find help (see ?shiny), the best way forward now may be to stop reading and start programming!
The recommended next step is to open the previously mentioned CycleHireApp/app.R script in an IDE of choice, modify it and re-run it repeatedly.
The example contains some of the components of a web mapping application implemented in shiny and should ‘shine’ a light on how they behave.

The CycleHireApp/app.R script contains shiny functions that go beyond those demonstrated in the simple ‘lifeApp’ example.
These include reactive() and observe() (for creating outputs that respond to the user interface — see ?reactive) and leafletProxy() (for modifying a leaflet object that has already been created).
Such elements are critical to the creation of web mapping applications implemented in shiny.
A range of ‘events’ can be programmed including advanced functionality such as drawing new layers or subsetting data, as described in the shiny section of RStudio’s leaflet website.

\BeginKnitrBlock{rmdnote}

There are a number of ways to run a shiny app.
For RStudio users, the simplest way is probably to click on the ‘Run App’ button located in the top right of the source pane when an app.R, ui.R or server.R script is open.
shiny apps can also be initiated by using runApp() with the first argument being the folder containing the app code and data: runApp("CycleHireApp") in this case (which assumes a folder named CycleHireApp containing the app.R script is in your working directory).
You can also launch apps from a Unix command line with the command Rscript -e 'shiny::runApp("CycleHireApp")'.
\EndKnitrBlock{rmdnote}

Experimenting with apps such as CycleHireApp will build not only your knowledge of web mapping applications in R, but also your practical skills.
Changing the contents of setView(), for example, will change the starting bounding box that the user sees when the app is initiated.
Such experimentation should not be done at random, but with reference to relevant documentation, starting with ?shiny, and motivated by a desire to solve problems such as those posed in the exercises.

shiny used in this way can make prototyping mapping applications faster and more accessible than ever before (deploying shiny apps, https://shiny.rstudio.com/deploy/, is a separate topic beyond the scope of this chapter).
Even if your applications are eventually deployed using different technologies, shiny undoubtedly allows web mapping applications to be developed in relatively few lines of code (86 in the case of CycleHireApp).
That does not stop shiny apps getting rather large.
The Propensity to Cycle Tool (PCT) hosted at pct.bike, for example, is a national mapping tool funded by the UK’s Department for Transport.
The PCT is used by dozens of people each day and has multiple interactive elements based on more than 1000 lines of code.

While such apps undoubtedly take time and effort to develop, shiny provides a framework for reproducible prototyping that should aid the development process.
One potential problem with the ease of developing prototypes with shiny is the temptation to start programming too early, before the purpose of the mapping application has been envisioned in detail.
For that reason, despite advocating shiny, we recommend starting with the longer established technology of a pen and paper as the first stage for interactive mapping projects.
This way your prototype web applications should be limited not by technical considerations, but by your motivations and imagination.

(\#fig:CycleHireApp-html)CycleHireApp, a simple web mapping application for finding the closest cycle hiring station based on your location and requirement of cycles. Interactive version available online at r.geocompx.org.

其他地图包

tmap provides a powerful interface for creating a wide range of static maps (Section @ref(static-maps)) and also supports interactive maps (Section @ref(interactive-maps)).
But there are many other options for creating maps in R.
The aim of this section is to provide a taster of some of these and pointers for additional resources: map making is a surprisingly active area of R package development, so there is more to learn than can be covered here.

The most mature option is to use plot() methods provided by core spatial packages sf and terra, covered in Sections @ref(basic-map) and @ref(basic-map-raster), respectively.
What we have not mentioned in those sections was that plot methods for vector and raster objects can be combined when the results draw onto the same plot area (elements such as keys in sf plots and multi-band rasters will interfere with this).
This behavior is illustrated in the subsequent code chunk which generates Figure @ref(fig:nz-plot).
plot() has many other options which can be explored by following links in the ?plot help page and the sf vignette sf5.

1
2
3
4
g = st_graticule(nz, lon = c(170, 175), lat = c(-45, -40, -35))
plot(nz_water, graticule = g, axes = TRUE, col = "blue")
terra::plot(nz_elev / 1000, add = TRUE, axes = FALSE)
plot(st_geometry(nz), add = TRUE)
Map of New Zealand created with plot(). The legend to the right refers to elevation (1000 m above sea level).

(\#fig:nz-plot)Map of New Zealand created with plot(). The legend to the right refers to elevation (1000 m above sea level).

The tidyverse\index{tidyverse (package)} plotting package ggplot2 also supports sf objects with geom_sf()\index{ggplot2 (package)}.
The syntax is similar to that used by tmap: an initial ggplot() call is followed by one or more layers, that are added with + geom_*(), where * represents a layer type such as geom_sf() (for sf objects) or geom_points() (for points).

ggplot2 plots graticules by default.
The default settings for the graticules can be overridden using scale_x_continuous(), scale_y_continuous() or coord_sf(datum = NA).
Other notable features include the use of unquoted variable names encapsulated in aes() to indicate which aesthetics vary and switching data sources using the data argument, as demonstrated in the code chunk below which creates Figure @ref(fig:nz-gg2):

1
2
3
4
5
library(ggplot2)
g1 = ggplot() + geom_sf(data = nz, aes(fill = Median_income)) +
geom_sf(data = nz_height) +
scale_x_continuous(breaks = c(170, 175))
g1

Another benefit of maps based on ggplot2 is that they can easily be given a level of interactivity when printed using the function ggplotly() from the plotly package\index{plotly (package)}.
Try plotly::ggplotly(g1), for example, and compare the result with other plotly mapping functions described at: blog.cpsievert.me.

An advantage of ggplot2 is that it has a strong user community and many add-on packages.
It includes ggspatial, which enhances ggplot2’s mapping capabilities by providing options to add a north arrow (annotation_north_arrow()) and a scale bar (annotation_scale()), or to add background tiles (annotation_map_tile()).
It also accepts various spatial data classes with layer_spatial().
Thus, we are able to plot SpatRaster objects from terra using this function as seen in Figure @ref(fig:nz-gg2).

1
2
3
4
5
6
7
library(ggspatial)
ggplot() +
layer_spatial(nz_elev) +
geom_sf(data = nz, fill = NA) +
annotation_scale() +
scale_x_continuous(breaks = c(170, 175)) +
scale_fill_continuous(na.value = NA)
1
2
3
4
5
6
7
8
9
10
11
12
13
library(ggplot2)
g1 = ggplot() + geom_sf(data = nz, aes(fill = Median_income)) +
geom_sf(data = nz_height) +
scale_x_continuous(breaks = c(170, 175))
g1
library(ggspatial)
ggplot() +
layer_spatial(nz_elev) +
geom_sf(data = nz, fill = NA) +
annotation_scale() +
scale_x_continuous(breaks = c(170, 175)) +
scale_fill_continuous(na.value = NA)
#> Warning: Removed 1348639 rows containing missing values (`geom_raster()`).
Comparison of map of New Zealand created with ggplot2 alone (left) and ggplot2 and ggspatial (right).Comparison of map of New Zealand created with ggplot2 alone (left) and ggplot2 and ggspatial (right).

(\#fig:nz-gg2)Comparison of map of New Zealand created with ggplot2 alone (left) and ggplot2 and ggspatial (right).

At the same time, ggplot2 has a few drawbacks, for example the geom_sf() function is not always able to create a desired legend to use from the spatial data.
Good additional ggplot2 resources can be found in the open source ggplot2 book and in the descriptions of the multitude of ‘ggpackages’ such as ggrepel and tidygraph.

We have covered mapping with sf, terra and ggplot2 first because these packages are highly flexible, allowing for the creation of a wide range of static maps.
Before we cover mapping packages for plotting a specific type of map (in the next paragraph), it is worth considering alternatives to the packages already covered for general-purpose mapping (Table @ref(tab:map-gpkg)).

(\#tab:map-gpkg)Selected general-purpose mapping packages.
Package Title
ggplot2 Create Elegant Data Visualisations Using the Grammar of Graphics
googleway Accesses Google Maps APIs to Retrieve Data and Plot Maps
ggspatial Spatial Data Framework for ggplot2
leaflet Create Interactive Web Maps with Leaflet
mapview Interactive Viewing of Spatial Data in R
plotly Create Interactive Web Graphics via 'plotly.js'
rasterVis Visualization Methods for Raster Data
tmap Thematic Maps

Table @ref(tab:map-gpkg) shows a range of mapping packages are available, and there are many others not listed in this table.
Of note is mapsf, which can generate range of geographic visualizations including choropleth, ‘proportional symbol’ and ‘flow’ maps.
These are documented in the mapsf\index{mapsf (package)} vignette.

Several packages focus on specific map types, as illustrated in Table @ref(tab:map-spkg).
Such packages create cartograms that distort geographical space, create line maps, transform polygons into regular or hexagonal grids, visualize complex data on grids representing geographic topologies, and create 3D visualizations.

(\#tab:map-spkg)Selected specific-purpose mapping packages, with associated metrics.
Package Title
cartogram Create Cartograms with R
geogrid Turn Geospatial Polygons into Regular or Hexagonal Grids
geofacet 'ggplot2' Faceting Utilities for Geographical Data
linemap Line Maps
tanaka Design Shaded Contour Lines (or Tanaka) Maps
rayshader Create Maps and Visualize Data in 2D and 3D

All of the aforementioned packages, however, have different approaches for data preparation and map creation.
In the next paragraph, we focus solely on the cartogram package\index{cartogram (package)}.
Therefore, we suggest to read the linemap\index{linemap (package)}, geogrid\index{geogrid (package)}, geofacet\index{geofacet (package)}, and rayshader\index{rayshader (package)} documentations to learn more about them.

A cartogram is a map in which the geometry is proportionately distorted to represent a mapping variable.
Creation of this type of map is possible in R with cartogram, which allows for creating continuous and non-contiguous area cartograms.
It is not a mapping package per se, but it allows for construction of distorted spatial objects that could be plotted using any generic mapping package.

The cartogram_cont() function creates continuous area cartograms.
It accepts an sf object and name of the variable (column) as inputs.
Additionally, it is possible to modify the intermax argument - maximum number of iterations for the cartogram transformation.
For example, we could represent median income in New Zeleand’s regions as a continuous cartogram (the right-hand panel of Figure @ref(fig:cartomap1)) as follows:

1
2
3
library(cartogram)
nz_carto = cartogram_cont(nz, "Median_income", itermax = 5)
tm_shape(nz_carto) + tm_polygons("Median_income")
1
2
3
4
5
#> Warning: Some legend items or map compoments do not fit well (e.g. due to the
#> specified font size).

#> Warning: Some legend items or map compoments do not fit well (e.g. due to the
#> specified font size).
Comparison of standard map (left) and continuous area cartogram (right).

(\#fig:cartomap1)Comparison of standard map (left) and continuous area cartogram (right).

cartogram also offers creation of non-contiguous area cartograms using cartogram_ncont() and Dorling cartograms using cartogram_dorling().
Non-contiguous area cartograms are created by scaling down each region based on the provided weighting variable.
Dorling cartograms consist of circles with their area proportional to the weighting variable.
The code chunk below demonstrates creation of non-contiguous area and Dorling cartograms of US states’ population (Figure @ref(fig:cartomap2)):

1
2
3
us_states2163 = st_transform(us_states, "EPSG:2163")
us_states2163_ncont = cartogram_ncont(us_states2163, "total_pop_15")
us_states2163_dorling = cartogram_dorling(us_states2163, "total_pop_15")
1
2
3
<!--

-->

练习

These exercises rely on a new object, africa.
Create it using the world and worldbank_df datasets from the spData package as follows:

1
2
3
4
5
6
7
8
library(spData)
africa = world |>
filter(continent == "Africa", !is.na(iso_a2)) |>
left_join(worldbank_df, by = "iso_a2") |>
select(name, subregion, gdpPercap, HDI, pop_growth) |>
st_transform("ESRI:102022") |>
st_make_valid() |>
st_collection_extract("POLYGON")

We will also use zion and nlcd datasets from spDataLarge:

1
2
zion = read_sf((system.file("vector/zion.gpkg", package = "spDataLarge")))
nlcd = rast(system.file("raster/nlcd.tif", package = "spDataLarge"))

E1. Create a map showing the geographic distribution of the Human Development Index (HDI) across Africa with base graphics (hint: use plot()) and tmap packages (hint: use tm_shape(africa) + ...).
- Name two advantages of each based on the experience.
- Name three other mapping packages and an advantage of each.
- Bonus: create three more maps of Africa using these three other packages.

E2. Extend the tmap created for the previous exercise so the legend has three bins: “High” (HDI above 0.7), “Medium” (HDI between 0.55 and 0.7) and “Low” (HDI below 0.55).
- Bonus: improve the map aesthetics, for example by changing the legend title, class labels and color palette.

E3. Represent africa’s subregions on the map.
Change the default color palette and legend title.
Next, combine this map and the map created in the previous exercise into a single plot.

E4. Create a land cover map of the Zion National Park.
- Change the default colors to match your perception of the land cover categories
- Add a scale bar and north arrow and change the position of both to improve the map’s aesthetic appeal
- Bonus: Add an inset map of Zion National Park’s location in the context of the Utah state. (Hint: an object representing Utah can be subset from the us_states dataset.)

E5. Create facet maps of countries in Eastern Africa:
- With one facet showing HDI and the other representing population growth (hint: using variables HDI and pop_growth, respectively)
- With a ‘small multiple’ per country

E6. Building on the previous facet map examples, create animated maps of East Africa:
- Showing each country in order
- Showing each country in order with a legend showing the HDI

E7. Create an interactive map of HDI in Africa:
- With tmap
- With mapview
- With leaflet
- Bonus: For each approach, add a legend (if not automatically provided) and a scale bar

E8. Sketch on paper ideas for a web mapping app that could be used to make transport or land-use policies more evidence based:
- In the city you live, for a couple of users per day
- In the country you live, for dozens of users per day
- Worldwide for hundreds of users per day and large data serving requirements

E9. Update the code in coffeeApp/app.R so that instead of centering on Brazil the user can select which country to focus on:
- Using textInput()
- Using selectInput()

E10. Reproduce Figure 9.1 and Figure 9.7 as closely as possible using the ggplot2 package.

E11. Join us_states and us_states_df together and calculate a poverty rate for each state using the new dataset.
Next, construct a continuous area cartogram based on total population.
Finally, create and compare two maps of the poverty rate: (1) a standard choropleth map and (2) a map using the created cartogram boundaries.
What is the information provided by the first and the second map?
How do they differ from each other?

E12. Visualize population growth in Africa.
Next, compare it with the maps of a hexagonal and regular grid created using the geogrid package.

本文由SCY翻译自《Geocomputation with R》第八章

本章关于读取和写入地理数据。地理数据输入对地理计算至关重要,没有数据就不可能有真实世界的应用。数据输出也非常关键,它能够让其他人使用由你的工作产生的有价值的新数据集或改进后的数据集。总而言之,输入/输出这些进程可以称为数据I/O。

阅读全文 »

本文由SCY翻译自《Geocomputation with R》第七章

本章演示了如何设置并将地理数据从一个CRS转换到另一个CRS,并进一步突出了由于忽略CRS而可能出现的特定问题,当您的数据以经纬度坐标存储时,应该特别注意。

阅读全文 »

本文由SCY翻译自《Geocomputation with R》第六章

本章专注于栅格和矢量地理数据模型之间的相互作用。包括四个主要技术:使用矢量对象进行栅格裁剪和遮罩;使用不同类型的矢量数据提取栅格值;以及栅格与矢量之间的转换。以上概念使用前几章中使用的数据进行演示,以了解其潜在的现实应用。

阅读全文 »

本文由SCY翻译自《Geocomputation with R》第五章

本章重点介绍如何操作地理对象的地理元素,例如通过简化和转换矢量几何图形、裁剪栅格数据集以及将矢量对象转换为栅格并从栅格转换为矢量。阅读本章并尝试章末的练习后,你应该能理解并控制sf对象中的几何列,以及与其他地理对象相关的栅格中所表示像素的范围和地理位置。

阅读全文 »

本文由SCY翻译自《Geocomputation with R》第四章

空间操作,包括矢量数据集之间的空间连接以及栅格数据集上的局部和焦点操作,是地理计算的重要部分。本章展示了如何基于空间对象的位置和形状以多种方式进行修改。许多空间操作有与之对应的非空间(属性)操作,因此上一章中演示的如子集提取和数据集连接等概念在此也适用。

阅读全文 »
0%