R 语言:使用夜间灯光数据构造城市形态指标

在「城市空间结构与劳动者工资收入(刘修岩等,2019)」一文中,作者提出了三个描述城市区域在地面投影的平面几何形状特征的指标:

  • road:城市中所有夜间灯光栅格之间地表直线距离的均值除以城市面积的平方根。反映城市内各地点之间的平均相互距离,代表绝大多数居民的出行成本。
  • max:城市内所有夜间灯光栅格间距中的最大值除以城市面积的平方根。表征城市可能出现的最远距离,突出”形态不规则”或”多中心”特征。
  • center:城市内每个夜间灯光栅格离城市中心点地表直线距离的均值除以城市面积的平方根。衡量城市中心的通达性,值越大说明大多数居民到市中心距离越远。

这三个指标从不同角度反映城市内部空间距离:road 关注平均距离,max 关注极端距离,center 关注中心可达性。指标值越高,城市形态越”劣质”。研究表明,城市形态通过影响居民出行成本和企业运输效率,对城市经济绩效和劳动者工资收入产生显著影响。

灯光阈值选择依据

夜间灯光数据记录了城市、城镇和乡村中相对稳定的灯光,年度夜光数据分辨率为 1kmx1km。作者选择亮度阈值40来界定城市化区域,基于以下考虑:

  1. 国际比较基准:Harari(2015) 对印度城市化区域的判定标准为亮度大于 35。考虑到中国的经济发展水平和能源消耗水平高于印度,我们适当提高阈值至40。
  2. 实证稳健性检验:使用 35 作为阈值重新计算城市形态指标后,研究结论保持一致,说明结果不依赖于特定阈值选择。
  3. 数据质量保证:阈值 40 能有效过滤乡村和小城镇的零星灯光,聚焦于真正的城市化区域。

本课程中将以 40 为例展示这几个指标的 R 语言计算过程。

数据处理流程与 R 代码详解

数据准备与重采样

夜间灯光数据使用的是平台上分享的这个:https://rstata.duanshu.com/#/brief/course/2c3880893f5947a5bf764befd3994c74,原始栅格数据可以从 Harvard dataverse 下载:https://dataverse.harvard.edu/dataset.xhtml?persistentId=doi:10.7910/DVN/GIYGJU

这里我仅仅选择 2022~2024 年的数据演示:

library(tidyverse)
library(sf)
library(terra)

raw-data 里面存放的是 2022~2024 年的夜间灯光栅格数据,不过存在各年数据范围、分辨率不一致的问题,不方便进行批量运算,因此需要先通过重采样对齐:

fs::dir_ls("raw-data") -> ls
rast(ls[length(ls)]) -> rsttemplate
dir.create("newrst")
lapply(1:length(ls), function(x){
print(x)
rast(ls[x]) %>%
terra::resample(rsttemplate) %>%
writeRaster(paste0("newrst/", str_extract(ls[x], "\\d{4}"), ".tif"))
}) -> res
# 这样就可以一次性读取全部栅格数据文件了
rast(fs::dir_ls("newrst")) -> rst

rst

#> class : SpatRaster
#> dimensions : 4055, 4856, 3 (nrow, ncol, nlyr)
#> resolution : 1000, 1000 (x, y)
#> extent : -2643773, 2212227, 1871897, 5926897 (xmin, xmax, ymin, ymax)
#> coord. ref. : WGS_1984_Albers
#> sources : 2022.tif
#> 2023.tif
#> 2024.tif
#> names : DMSP2022, DMSP2023, DMSP2024
#> min values : 0, 0, 0
#> max values : 63, 63, 63

按照作者的定义:城市区域为市辖区灯光亮度大于 40 的:

# 读取县级行政区划(里面有县类型变量)
read_sf("2021行政区划/县.shp") %>%
st_make_valid() -> county

# 合并每个城市市辖区的部分
county %>%
filter(县类型 == "市辖区") %>%
group_by(省, 省代码, 市, 市代码) %>%
summarise() %>%
ungroup() -> city

city

# 夜间灯光数据
library(terra)
fs::dir_ls("newrst") %>%
rast() -> rst

rst
#> class : SpatRaster
#> dimensions : 4055, 4856, 3 (nrow, ncol, nlyr)
#> resolution : 1000, 1000 (x, y)
#> extent : -2643773, 2212227, 1871897, 5926897 (xmin, xmax, ymin, ymax)
#> coord. ref. : WGS_1984_Albers
#> sources : 2022.tif
#> 2023.tif
#> 2024.tif
#> names : DMSP2022, DMSP2023, DMSP2024
#> min values : 0, 0, 0
#> max values : 63, 63, 63

# 保持矢量数据和栅格数据坐标参考系一致:
city %>%
st_transform(crs(rst)) -> city
city

#> Simple feature collection with 293 features and 4 fields
#> Geometry type: GEOMETRY
#> Dimension: XY
#> Bounding box: xmin: 84.57693 ymin: 3.83703 xmax: 131.9267 ymax: 50.98134
#> Geodetic CRS: WGS 84
#> # A tibble: 293 × 5
#> 省 省代码 市 市代码 geometry
#> <chr> <dbl> <chr> <dbl> <GEOMETRY [°]>
#> 1 上海市 310000 上海市 310000 MULTIPOLYGON (((121.9953 31.16712, 122…
#> 2 云南省 530000 临沧市 530900 POLYGON ((100.3887 23.95409, 100.3872 …
#> 3 云南省 530000 丽江市 530700 POLYGON ((100.445 27.16406, 100.4444 2…
#> 4 云南省 530000 保山市 530500 POLYGON ((99.32547 25.51959, 99.3251 2…
#> 5 云南省 530000 昆明市 530100 MULTIPOLYGON (((103.0588 26.53958, 103…
#> 6 云南省 530000 昭通市 530600 POLYGON ((103.9223 27.58819, 103.9226 …
#> 7 云南省 530000 普洱市 530800 POLYGON ((101.2431 22.79965, 101.2419 …
#> 8 云南省 530000 曲靖市 530300 POLYGON ((104.013 25.50248, 104.0135 2…
#> 9 云南省 530000 玉溪市 530400 POLYGON ((102.8801 24.52073, 102.8751 …
#> 10 内蒙古自治区 150000 乌兰察布市 150900 POLYGON ((113.2284 41.17408, 113.2188 …
#> # ℹ 283 more rows

然后就可以分区域计算每个城市中夜光亮度大于等于 40 的面积了:

# 计算每个栅格的面积
cellSize(rst, unit = "km") -> arearst

arearst[rst < 40] <- NA
names(arearst) <- names(rst)
plot(arearst)

可以看出在当前坐标系下各个像元的面积基本是一样的,不过我们还是直接加总每个城市区域里面的栅格面积,而不是仅仅统计数量:

terra::extract(arearst, vect(city),
fun = "sum", na.rm = T) %>%
as_tibble() -> res

bind_cols(city, res) %>%
select(-ID) %>%
st_drop_geometry() %>%
gather(contains("DMSP"), key = "year", value = "area") %>%
mutate(year = str_remove_all(year, "DMSP"),
year = as.numeric(year)) -> areadf

areadf
#> # A tibble: 879 × 6
#> 省 省代码 市 市代码 year area
#> <chr> <dbl> <chr> <dbl> <dbl> <dbl>
#> 1 上海市 310000 上海市 310000 2022 5651.
#> 2 云南省 530000 临沧市 530900 2022 47.0
#> 3 云南省 530000 丽江市 530700 2022 88.0
#> 4 云南省 530000 保山市 530500 2022 82.0
#> 5 云南省 530000 昆明市 530100 2022 1012.
#> 6 云南省 530000 昭通市 530600 2022 101.
#> 7 云南省 530000 普洱市 530800 2022 69.0
#> 8 云南省 530000 曲靖市 530300 2022 289.
#> 9 云南省 530000 玉溪市 530400 2022 144.
#> 10 内蒙古自治区 150000 乌兰察布市 150900 2022 150.
#> # ℹ 869 more rows

road、max、center 指标

然后就可以计算各年各城市的三个指标了:

# 计算区域内栅格单元间平均直线距离的函数
mean_distance <- function(r, na.rm = T) {
dist_matrix <- terra::distance(as.points(r),as.points(r))
mean(dist_matrix[lower.tri(dist_matrix)], na.rm = T)
}

# 最大距离函数也可以顺便写好:
max_distance <- function(r, na.rm = T) {
dist_matrix <- terra::distance(as.points(r),as.points(r))
max(dist_matrix[lower.tri(dist_matrix)], na.rm = T)
}

# 城市区域
rst[rst < 40] <- NA

rst
#> class : SpatRaster
#> dimensions : 4055, 4856, 3 (nrow, ncol, nlyr)
#> resolution : 1000, 1000 (x, y)
#> extent : -2643773, 2212227, 1871897, 5926897 (xmin, xmax, ymin, ymax)
#> coord. ref. : WGS_1984_Albers
#> source(s) : memory
#> names : DMSP2022, DMSP2023, DMSP2024
#> min values : 40, 40, 40
#> max values : 63, 63, 63

# 第一个城市
fc <- city[1,]
fc
#> Simple feature collection with 1 feature and 4 fields
#> Geometry type: MULTIPOLYGON
#> Dimension: XY
#> Bounding box: xmin: 1485102 ymin: 3373740 xmax: 1607977 ymax: 3506828
#> Projected CRS: WGS_1984_Albers
#> # A tibble: 1 × 5
#> 省 省代码 市 市代码 geometry
#> <chr> <dbl> <chr> <dbl> <MULTIPOLYGON [m]>
#> 1 上海市 310000 上海市 310000 (((1589979 3439151, 1590292 3440561, 1590559 3441…

# 提取该城市的城市区域
rst %>%
crop(vect(fc)) %>%
mask(vect(fc)) -> frst

# 计算该城市所有年份的这样循环即可
lapply(1:nlyr(frst), function(x){
mean_distance(frst[[x]], na.rm = T)
}) -> tempres
unlist(tempres)

#> [1] 41912.46 44974.35 43500.98

# center 指标的计算示例:

frst[[1]] %>%
as.polygons() %>%
st_as_sf() %>%
st_union() %>%
st_cast("POLYGON") %>%
st_sf() %>%
mutate(area = st_area(.)) %>%
arrange(desc(area)) %>%
slice(1) %>%
st_centroid() -> centroids

centroids

plot(frst[[1]])
plot(centroids, add = T, col = "red")

然后就可以计算所有年份、所有城市的:

dir.create("res")
lapply(1:nrow(city), function(y){
fc <- city[y,]
rst %>%
crop(vect(fc)) %>%
mask(vect(fc)) -> frst

lapply(1:nlyr(frst), function(x){
if(!is.na(mean(frst[[x]][], na.rm = T))) {

frst[[x]] %>%
as.polygons() %>%
st_as_sf() %>%
st_union() %>%
st_cast("POLYGON") %>%
st_sf() %>%
mutate(area = st_area(.)) %>%
arrange(desc(area)) %>%
slice(1) %>%
st_centroid() -> centroids

# 所有栅格和这个点的距离
frst[[x]] %>%
as.points() %>%
distance(vect(centroids)) -> distmat

fc %>%
st_drop_geometry() %>%
mutate(mean = mean_distance(frst[[x]], na.rm = T),
max = max_distance(frst[[x]], na.rm = T),
center = mean(distmat, na.rm = T) / 1000,
year = as.numeric(str_extract(names(frst)[x], "\\d{4}"))) %>%
write_rds(paste0("res/", y, "_", x, ".rds"))
}
}) -> tempres1
}) -> tempres2

合并计算结果:

fs::dir_ls("res") %>%
lapply(readr::read_rds) %>%
bind_rows() -> df2
df2

#> # A tibble: 873 × 8
#> 省 省代码 市 市代码 mean max center year
#> <chr> <dbl> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 广东省 440000 梅州市 441400 13004. 53235. 8.62 2022
#> 2 广东省 440000 梅州市 441400 8003. 18028. 5.87 2023
#> 3 广东省 440000 梅州市 441400 12091. 53712. 8.12 2024
#> 4 广东省 440000 汕头市 440500 26953. 78102. 20.1 2022
#> 5 广东省 440000 汕头市 440500 27456. 80281. 20.2 2023
#> 6 广东省 440000 汕头市 440500 27219. 79649. 20.1 2024
#> 7 广东省 440000 汕尾市 441500 11761. 36878. 8.43 2022
#> 8 广东省 440000 汕尾市 441500 8933. 23431. 6.53 2023
#> 9 广东省 440000 汕尾市 441500 10895. 35609. 7.72 2024
#> 10 广东省 440000 江门市 440700 20917. 69721. 15.1 2022
#> # ℹ 863 more rows

再合并两部分计算结果:

df2 %>%
full_join(areadf) %>%
mutate(road = mean / (1000 * sqrt(area)),
max = max / (1000 * sqrt(area)),
center = center / (sqrt(area))) %>%
select(-mean) %>%
select(省, 省代码, 市, 市代码, year, everything()) %>%
arrange(省, 省代码, 市, 市代码, year) %>%
rename(年份 = year) %>%
haven::write_dta("2022~2024年各城市形态指标面板数据.dta", label = "数据处理:微信公众号 RStata")

这里需要注意 mean 和 max 本来的单位是 m,所以除以 1000,center 我计算的单位就是 km 就不用再除以 1000 了。

点击这里跳转到 RStata 短书平台获取附件:R 语言:使用夜间灯光数据构造城市形态指标

评论