查看原文
其他

使用 Stata 爬取全国宗庙祠堂数据并绘图展示

RStata RStata 2023-10-24

由于之前的思路不太好,这里又重新更新了下~

该课程已经讲解完,感兴趣的小伙伴可以点击文末的阅读原文跳转到平台上观看学习!视频讲解中使用的是 utm 坐标系,这里改用了 aea 坐标系,这样由于坐标系转换产生的距离变化会更小,课程里面的检索结果更多是因为 utm 转换会 wgs84 后的格点实际上更细密了。

最近有个小伙伴想要获取全国宗庙祠堂数据,还附带给我转发了一篇推文:绘制全国祠堂密度地图

这篇推文是使用 Stata 爬取的,具体思路是先计算各区县的经纬度,然后使用百度地图的“地点检索”接口搜索附近 50km 范围内的宗庙祠堂。不过显然这种方法会遗漏很多,主要是:

  1. 这个范围会遗漏中国的很多地区;
  2. 50km 的范围太大,百度地图只会给出该范围的一些热门的搜索结果。

因此我们需要生成更细密的检索中心,然后使用更小的搜索半径。

百度地图的地点检索接口文档在这里:https://lbsyun.baidu.com/faq/api?title=webapi/guide/webservice-placeapi/circle ,虽然多边形检索看起来更高效,不过这个接口需要单独申请,所以我们还是只能使用圆形区域检索。

在使用地点检索之前,我们需要先构建一个经纬度中心点数据。

为了便于演示,在这里我们依然使用 50km 的半径进行检索,由于检索是圆形检索,所以为了不遗漏,我们需要建立分辨率与 50√2km x 50√2km 的经纬度网格,这样以每个网格的中心为检索中心、范围是 50km 的圆形区域恰好可以覆盖这一区域,如下图所示:

由于 Stata 中地理计算的工具不完善,所以我们需要借助 R 语言来生成这样的一个网格。

不会使用 R 语言的小伙伴也不必担心,附件中提供了最终的结果数据。

首先加载所需的 R 包:

library(tidyverse)
library(sf)

合并各省份的数据得到中国范围的矢量数据:

# 生成中国范围的矢量数据
read_sf("2020行政区划/省.shp") %>% 
  st_union() -> cn 
nngeo::st_remove_holes(cn[2]) -> cn 

这个中国的范围是这样的:

url <- "http://map.geoq.cn/ArcGIS/rest/services/ChinaOnlineCommunity/MapServer/tile/{z}/{y}/{x}"
library(leaflet) 
leaflet() %>% 
  addTiles(url, attribution = "微信公众号 RStata") -> map 
mapview::mapview(cn, map = map, layer.name = "中国范围")

创建一些边长为 50√2 km的格网,这样每个方格正好可以使用一个半径 50km 的圆覆盖。转换成以 m 为单位的坐标系更容易创建:

# 这里以讲义材料的这个代码为准
mycrs <- "+proj=aea +lat_0=0 +lon_0=105 +lat_1=25 +lat_2=47 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs" 
cn %>% 
  st_transform(mycrs) -> cnaea
cnaea %>% 
  st_make_grid(cellsize = units::set_units(c(50 * sqrt(2), 50 * sqrt(2)), "km")) -> cngrid

这个网格的效果是这样的:

# 效果是这样的
read_sf("九段线.geojson") %>% 
  st_transform(mycrs) -> jdxaea

plot(cngrid)
plot(cnaea, add = T, col = "red"
plot(jdxaea, add = T, col = "black"

提取与中国相交的格点:

cngrid %>% 
  st_intersection(cnaea) -> cngrid 

plot(cngrid) 

计算这些格点的质心:

cngrid %>% 
  st_sf() %>% 
  st_centroid() %>% 
  st_transform(4326) %>% 
  st_coordinates() %>% 
  as_tibble() %>% 
  mutate(id = row_number()) -> coorddf 

# 保存
coorddf %>% 
  haven::write_dta("50km 网格中心.dta"

这样我们就得到了检索中心坐标。另外附件中还提供了 10km 分辨率的网格中心数据。

再回到 Stata 中。

首先我们看一下百度地图地理检索接口如何使用,例如搜索 (39.915,116.404) 附近 2km 的 宗祠家庙祠堂 可以使用下面的链接进行请求:

clear all 
*- "a8niO5h8icau3IRNG2Wv2k5CduVyrktL" 是我的百度地图密钥,需要替换成自己的
copy "https://api.map.baidu.com/place/v2/search?query=宗祠$家庙$祠堂&location=39.915,116.404&radius=2000&output=json&ak=a8niO5h8icau3IRNG2Wv2k5CduVyrktL&coord_tyle=wgs84ll&ret_coordtype=gcj02ll&page_size=20&page_num=0" "temp.json"replace 

这里使用了这些参数:

  • query=宗祠祠堂(检索关键词)
  • location=39.915,116.404(检索中心)
  • radius=2000(检索半径 2km)
  • output=json(返回结果为 json 格式)
  • ak=a8niO5h8icau3IRNG2Wv2k5CduVyrktL(用户密钥,注意要替换成自己的)
  • coord_tyle=wgs84ll(检索中心经纬度的坐标系)
  • ret_coordtype=gcj02ll(返回结果中经纬度的坐标系)
  • page_size=20(每次请求的最大数量,最大只能为 20)
  • page_num=0(0 表示第一页,如果检索结果超过 20 ,需要循环多页爬取)。

有时候也会需要对中文进行 URL 转码,附件中提供了一个 perccentencode.ado 命令,可以进行 URL 转码:

*- 也可能需要对 query 部分进行 URL 转码 
percentencode 宗祠
local a = r(percentencode) 
percentencode 家庙
local b = r(percentencode) 
percentencode 祠堂
local c = r(percentencode) 

copy "https://api.map.baidu.com/place/v2/search?query=`a'$`b'$`c'&location=39.915,116.404&radius=2000&output=json&ak=a8niO5h8icau3IRNG2Wv2k5CduVyrktL&coord_tyle=wgs84ll&ret_coordtype=gcj02ll&page_size=20&page_num=0" "temp.json"replace 

这是个 json 文件,在课程「Stata 网络数据爬取:JSON篇」(https://rstata.duanshu.com/#/brief/course/c6de9fae65df4eeb814d2275545e224d) 中我系统的介绍过如何在 Stata 中处理 json 格式的数据,这里我们可以这样解析:

*- 查看响应
insheetjson using "temp.json", showr flatten 

*- 解析数据
clear 
gen str100 name = ""
gen str100 lat = ""
gen str100 lng = ""
gen str100 address = ""
gen str100 province = ""
gen str100 city = ""
gen str100 area = ""
gen str100 street_id = ""
gen str100 detail = ""
gen str100 uid = ""
insheetjson name lat lng address province city ///
  area street_id detail uid using "temp.json"///
  table("results") col("name" "location:lat" ///
    "location:lng" "address" "province" "city" ///
    "area" "street_id" "detail" "uid")
compress 
destringreplace 

这里仅仅处理了 page_num=0 页,如果结果超过 20(total)的话,就需要爬取多页了:

注意看接口文档中的 total 介绍:POI 检索总数,开发者请求中设置了 page_num 字段才会出现 total 字段。出于数据保护目的,单次请求 total 最多为150。

*- 第 2 页(如果有的话)
copy "https://api.map.baidu.com/place/v2/search?query=宗祠$家庙$祠堂&location=39.915,116.404&radius=2000&output=json&ak=a8niO5h8icau3IRNG2Wv2k5CduVyrktL&coord_tyle=wgs84ll&ret_coordtype=gcj02ll&page_size=20&page_num=1" "temp2.json"replace 

insheetjson name lat lng address ///
  province city area detail uid ///
  using temp2.json, table(results) ///
  col("name" "location:lat" "location:lng" "address" "province" ///
    "city" "area" "detail" "uid") offset(20) 

*- 第 3 页(如果有的话)
copy "https://api.map.baidu.com/place/v2/search?query=宗祠$家庙$祠堂&location=39.915,116.404&radius=2000&output=json&ak=a8niO5h8icau3IRNG2Wv2k5CduVyrktL&coord_tyle=wgs84ll&ret_coordtype=gcj02ll&page_size=20&page_num=2" "temp3.json"replace 

insheetjson name lat lng address ///
  province city area detail uid ///
  using temp3.json, table(results) ///
  col("name" "location:lat" "location:lng" "address" "province" ///
    "city" "area" "detail" "uid") offset(20) 

然后就可以循环爬取处理了。

对于复杂的网络数据爬取任务,建议先下载、下载完成后再集中解析。

首先循环下载首页 json 文件到工作目录下的 res 文件夹:

use "50km 网格中心.dta"clear 

*- 循环每个观测值 
cap mkdir "res" 

*- 爬取首页
forval i = 1/`=_N' {
  di "`i'"
  if !fileexists("res/`=id[`i']'_0.json") { 
    copy "https://api.map.baidu.com/place/v2/search?query=宗祠$家庙$祠堂&location=`=Y[`i']',`=X[`i']'&radius=50000&output=json&ak=a8niO5h8icau3IRNG2Wv2k5CduVyrktL&coord_tyle=wgs84ll&ret_coordtype=gcj02ll&page_size=20&page_num=0" "res/`=id[`i']'_0.json"replace 
  }

从这些首页 json 文件里面提取 total 值:

*- 提取 total 结果
clear all
gen str10 total = ""
insheetjson total using "temp.json"table("total") offset(0)

use "50km 网格中心.dta"clear 
tostring id, replace 
gen file = "res/" + id + "_0.json"
gen str10 total = ""
forval i = 1/`=_N' {
  insheetjson total using "`=file[`i']'"table("total") offset(`=`i' - 1')
}

destringreplace 

*- total <= 20 的都是只有一页的
drop if total <= 20 

gen pagenum = int(total/20)

*- 循环下载其余页面
forval i = 1/`=_N' {
  forval j = 1/`=pagenum[`i']' {
    if !fileexists("res/`=id[`i']'_`j'.json") {
      copy "https://api.map.baidu.com/place/v2/search?query=宗祠$家庙$祠堂&location=`=Y[`i']',`=X[`i']'&radius=50000&output=json&ak=a8niO5h8icau3IRNG2Wv2k5CduVyrktL&coord_tyle=wgs84ll&ret_coordtype=gcj02ll&page_size=20&page_num=`j'" "res/`=id[`i']'_`j'.json"replace 
    }
  }
}

循环处理下载得到的文件:

local files: dir "res" files "*.json"
di `"`files'"'

clear 
gen str100 name = ""
gen str100 lat = ""
gen str100 lng = ""
gen str100 address = ""
gen str100 province = ""
gen str100 city = ""
gen str100 area = ""
gen str100 detail = ""
gen str100 uid = "" 

foreach i in `files' {
  local N = `=_N'
  di "`i'"
  insheetjson name lat lng address province city ///
    area detail uid using "res/`i'"///
      table("results") col("name" "location:lat" ///
    "location:lng" "address" "province" "city" ///
    "area" "detail" "uid") offset(`N')
}

compress 
destringreplace 

foreach i of varlist _all {
  cap format `i' %10s
}

*- 删除重复的
duplicates drop _all, force 

另外因为得到结果里面的经纬度是 GCJ02 坐标系的,为了后续的地理计算,我们需要把它转换成 WGS84 坐标系的,附件中提供了一个转换命令:

gcj02towgs84, lon(lng) lat(lat) 
save "全国宗庙祠堂分布(50km范围的检索)"replace 

50km 的检索大概只得到了一万多个宗庙祠堂(实际上每次解析也可能得到不同的结果),在附件中我还提供了一份更全的结果,是使用 10km 范围进行检索的(使用的网格也是 10√2km 的)。下面我们绘图比较下两个结果:

*- 绘制地图(使用更全的一个版本)
*- 这个里面的 lat lng 是我转换成 WGS84 过的,不需要再转换了。
import excel using "全国宗庙祠堂分布(10km 范围的检索).xlsx"clear first 
foreach i of varlist _all {
  cap format `i' %10s
}
gen id = _n 
encode province, gen(prov)
*- ssc install geo2xy 
ren lat 纬度 
ren lng 经度 
geo2xy 纬度 经度, gen(lat lon) projection(albers, 6378137 298.257223563 25 47 0 105) replace
gen class = "main"
save mainpoint, replace 

*- 提取小地图框格的
use mainpoint, clear 
keep if inrange(lon, 120000, 1766004.1) & inrange(lat, 320000, 2557786.0)
replace lon = lon * 0.5 + 2100000
replace lat = lat * 0.5 + 1665139
replace class = "smallbox" 
append using mainpoint
save pointdata, replace 

use chinacity2020mini_db.dta, clear
drop if 省代码 == 0 
spmap using chinacity2020mini_coord.dta, id(ID) ///
  ocolor("black" ...) osize(vvthin ...) ///
    line(data(chinacity2020mini_line_coord2.dta) ///
    /// 胡焕庸线(7)秦岭淮河下(4)
    select(keep if inlist(group, 1, 2, 3, 5, 6)) ///
    by(group) size(vvthin *1 *0.5 *0.5 *0.5) ///
    pattern(solid ...) ///
    color(white /// 市界颜色
        black /// 国界线颜色
        "0 85 170" /// 海岸线颜色
        black /// 小地图框格颜色
        black /// 比例尺和指北针颜色
        )) ///
  polygon(data(polygon2) fcolor(black) ///
    osize(vvthin)) ///
  label(data(chinacity2020mini_label2) x(X) y(Y) label(cname) length(20) size(*0.8)) ///
    point(data(pointdata) by(prov) ///
      fcolor("80 80 255" "206 61 50" "116 155 88" ///
        "240 230 133" "70 105 131" "186 99 56" "93 177 221" ///
        "128 34 104" "107 215 107" "213 149 167" "146 72 34" ///
        "131 123 141" "199 81 39" "213 143 92" "122 101 165" ///
        "228 175 105" "59 27 83" "205 222 183" "97 42 121" ///
        "174 31 99" "231 199 111" "90 101 94" "204 153 0" ///
        "153 204 0" "169 169 169" "204 153 0" "153 204 0" ///
        "51 204 0" "0 204 51" "0 204 153" "0 153 204" ///
        "10 71 255" "71 117 255" "255 194 10"///
        x(lon) y(lat) ///
        size(*0.1 ...) legenda(off)) ///
    leg(off) ///
    ti("全国宗庙祠堂分布(10km 范围的检索)", color(black)) /// 
    subti("数据处理 & 绘制:微信公众号 RStata"///
    graphr(margin(medium)) ///
    caption("数据来源:百度地图地点检索接口", size(*0.8))
gr export "全国宗庙祠堂分布(10km 范围的检索).png"replace width(4800)

*- 50km 的
use "全国宗庙祠堂分布(50km范围的检索).dta"clear 
gen id = _n 
encode province, gen(prov)
*- ssc install geo2xy 
drop 纬度 经度 
ren 纬度_WGS84 纬度 
ren 经度_WGS84 经度 
geo2xy 纬度 经度, gen(lat lon) projection(albers, 6378137 298.257223563 25 47 0 105) replace
gen class = "main"
save mainpoint, replace 

*- 提取小地图框格的
use mainpoint, clear 
keep if inrange(lon, 120000, 1766004.1) & inrange(lat, 320000, 2557786.0)
replace lon = lon * 0.5 + 2100000
replace lat = lat * 0.5 + 1665139
replace class = "smallbox" 
append using mainpoint
save pointdata, replace 

use chinacity2020mini_db.dta, clear
drop if 省代码 == 0 
spmap using chinacity2020mini_coord.dta, id(ID) ///
  ocolor("black" ...) osize(vvthin ...) ///
    line(data(chinacity2020mini_line_coord2.dta) ///
    /// 胡焕庸线(7)秦岭淮河下(4)
    select(keep if inlist(group, 1, 2, 3, 5, 6)) ///
    by(group) size(vvthin *1 *0.5 *0.5 *0.5) ///
    pattern(solid ...) ///
    color(white /// 市界颜色
        black /// 国界线颜色
        "0 85 170" /// 海岸线颜色
        black /// 小地图框格颜色
        black /// 比例尺和指北针颜色
        )) ///
  polygon(data(polygon2) fcolor(black) ///
    osize(vvthin)) ///
  label(data(chinacity2020mini_label2) x(X) y(Y) label(cname) length(20) size(*0.8)) ///
    point(data(pointdata) by(prov) ///
      fcolor("80 80 255" "206 61 50" "116 155 88" ///
        "240 230 133" "70 105 131" "186 99 56" "93 177 221" ///
        "128 34 104" "107 215 107" "213 149 167" "146 72 34" ///
        "131 123 141" "199 81 39" "213 143 92" "122 101 165" ///
        "228 175 105" "59 27 83" "205 222 183" "97 42 121" ///
        "174 31 99" "231 199 111" "90 101 94" "204 153 0" ///
        "153 204 0" "169 169 169" "204 153 0" "153 204 0" ///
        "51 204 0" "0 204 51" "0 204 153" "0 153 204" ///
        "10 71 255" "71 117 255" "255 194 10"///
        x(lon) y(lat) ///
        size(*0.1 ...) legenda(off)) ///
    leg(off) ///
    ti("全国宗庙祠堂分布(50km 范围的检索)", color(black)) /// 
    subti("数据处理 & 绘制:微信公众号 RStata"///
    graphr(margin(medium)) ///
    caption("数据来源:百度地图地点检索接口", size(*0.8))
gr export "全国宗庙祠堂分布(50km 范围的检索).png"replace width(4800)

可以看到两个结果虽然数量上有差异,但是地理分布趋势相近。因此我猜测使用不全的数据结果很可能也可能得到靠谱的结果。所以如果只是准备采用这个数据作为某个变量的代理变量,不用太担心全不全的事情。

最后我们再计算下各个城市的宗庙祠堂密度,首先使用 R 语言计算各个城市的行政区划面积:

# 计算各市的面积
read_sf("2020行政区划/市.shp") -> city

city %>% 
  mutate(面积 = st_area(.)) %>% 
  st_drop_geometry() %>% 
  mutate(面积 = units::set_units(面积, km2),
         面积 = as.numeric(面积)) %>% 
  select(-contains("类型")) %>% 
  haven::write_dta("2020年各城市行政区划面积.dta")

然后再统计、计算、绘图即可:

*- 各地级市的宗庙祠堂密度
*- 首先根据经纬度判断所处的省市区县
shp2dta using 2020行政区划/县.shp, database(county_db) coordinates(county_coord) genid(ID) replace 

import excel using "全国宗庙祠堂分布(10km 范围的检索).xlsx"clear first 
geoinpoly lat lng using county_coord.dta 
ren _ID ID 
merge m:1 ID using county_db 
drop if missing(lng) 
drop 省 省代码 市 县 县代码 县类型 市类型 省类型 _merge
gen id = _n 
*- 计算每个城市的宗庙祠堂密度
collapse (countcount = id, by(市代码) 

*- 匹配面积数据
merge 1:1 市代码 using 2020年各城市行政区划面积.dta 
replace count = 0 if missing(count
keep count 市 市代码 面积
*- 面积的单位是平方公里
gen density = count / (面积 / 10000) 
label var density "宗庙祠堂密度(个/万平方公里)" 
save "各城市宗庙祠堂密度"replace 

*- 绘图
use chinacity2020mini_db.dta, clear
merge 1:1 市 市代码 using "各城市宗庙祠堂密度" 
keep if _m == 3 
replace density = 0 if missing(density)

grmap density using chinacity2020mini_coord.dta, ///
  id(ID) osize(vvthin ...) ocolor(white ...) ///
  clmethod(custom) clbreaks(0 3 9 26 48 80 686) ///
    fcolor("255 255 229" "247 252 185" "217 240 163" "173 221 142" "120 198 121" "65 171 93" "35 132 67" "0 90 50"/// 
    graphr(margin(medium)) ///
    leg(order(2 "<= 3个/万平方公里" 3 "3 ~ 9个/万平方公里"  ///
        4 "9 ~ 26个/万平方公里"  5 "26 ~ 48个/万平方公里" ///
        6 "48 ~ 80个/万平方公里" 7 "> 80个/万平方公里")) /// 
  line(data(chinaprov2010mini_line_coord2.dta) by(group) ///
    size(vvthin *1 *0.5 *0.5 *0.5) pattern(solid ...) ///
    select(drop if inlist(group, 4, 7)) ///
    color(gs20 /// 省界颜色 
        black /// 国界线颜色 
        "0 85 170" /// 海岸线颜色 
        black /// 小地图框格颜色
        black /// 比例尺和指北针颜色
        )) /// 
  polygon(data(polygon2) fcolor(black) ///
    osize(vvthin)) ///
  label(data(chinacity2020mini_label2) x(X) y(Y) label(cname) length(20) size(*0.8)) ///
  ti("各城市宗庙祠堂密度"/// 
  subti("数据整理 & 绘制:微信公众号 RStata"///
  caption("数据来源:百度地图地点检索接口", size(*0.8))

gr export "各城市宗庙祠堂密度(10km 范围的检索).png"replace width(4800)

这样我们就解决了这个问题。

课程信息

该课程已经讲解完,感兴趣的小伙伴可以点击文末的阅读原文跳转到平台上观看学习!

  1. 直播地址:腾讯会议(需要报名 RStata 培训班参加)
  2. 讲义材料:需要报名 RStata 培训班,详情可阅读:一起来学习 R 语言和 Stata 啦!学习过程中遇到的问题也可以随时提问!

更多关于 RStata 会员的更多信息可添加微信号 r_stata 咨询:

附件下载(点击文末的阅读原文即可跳转):https://rstata.duanshu.com/#/brief/course/1a86473ee3c14a1c8f4a1ac55d35d05d


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存