使用 R 语言爬取全国医疗机构列表

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

为了更全面的检索,我们可以先生成一个间距均匀的网格点坐标数据,这里为了方便演示,我们使用 50km 的间距(不过实际上 50km 的检索会遗漏很多结果,因为百度地图只会给出该范围的一些热门的搜索结果)。

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

生成间距均匀的网格点坐标数据

首先加载所需的 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
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 分辨率的网格中心数据(计算方法类似)。

百度地图地点检索接口

该接口的介绍文档在这里:https://lbsyun.baidu.com/faq/api?title=webapi/guide/webservice-placeapi/circle ,使用前需要先申请一个百度地图的 ak 密钥,在运行代码前大家需要把下面代码中的 a8niO5h8icau3IRNG2Wv2k5CduVyrktL 替换成自己的:

这里注意结合视频讲解学习,很多细节难以文字表述。

library(jsonlite)
# 测试百度地图地点检索功能
fromJSON("https://api.map.baidu.com/place/v2/search?query=医院$诊所$药店$体检$疗养院$急救中心$疾控中心$医疗&tag=医疗&location=39.915,116.404&radius=2000&output=json&ak=a8niO5h8icau3IRNG2Wv2k5CduVyrktL&coord_tyle=wgs84ll&ret_coordtype=gcj02ll&page_size=20&page_num=0") -> ls
ls$results %>%
as_tibble()

#> # A tibble: 20 × 10
#> name location$lat address province city area street_id telephone detail
#> <chr> <dbl> <chr> <chr> <chr> <chr> <chr> <chr> <int>
#> 1 北京市… 39.9 北京市… 北京市 北京… 东城… "4ea3f2f… (010)655… 1
#> 2 首都医… 39.9 北京市… 北京市 北京… 东城… "" (010)570… 1
#> 3 首都医… 39.9 北京市… 北京市 北京… 东城… "0adce3e… (010)522… 1
#> 4 北京医院 39.9 北京市… 北京市 北京… 东城… "" (010)652… 1
#> 5 北京同… 39.9 北京市… 北京市 北京… 东城… "2a64d1c… (010)670… 1
#> 6 北京王… 39.9 北京市… 北京市 北京… 东城… "fa7da3e… (010)851… 1
#> 7 北京拜… 39.9 北京市… 北京市 北京… 东城… "" (010)670… 1
#> 8 北京市… 39.9 北京市… 北京市 北京… 东城… "" (010)652… 1
#> 9 北京韩… 39.9 北京市… 北京市 北京… 西城… "" <NA> 1
#> 10 北京市… 39.9 北京市… 北京市 北京… 西城… "" (010)630… 1
#> 11 北京市… 39.9 北京市… 北京市 北京… 东城… "" (010)670… 1
#> 12 北京医… 39.9 北京市… 北京市 北京… 东城… "f96b45e… <NA> 1
#> 13 首都医… 39.9 北京市… 北京市 北京… 东城… "" <NA> 1
#> 14 北京同… 39.9 北京市… 北京市 北京… 东城… "" <NA> 1
#> 15 同仁堂… 39.9 北京市… 北京市 北京… 东城… "407ae2d… <NA> 1
#> 16 同仁堂… 39.9 北京市… 北京市 北京… 东城… "" <NA> 1
#> 17 北京妇… 39.9 北京市… 北京市 北京… 东城… "" <NA> 1
#> 18 北京王… 39.9 北京市… 北京市 北京… 东城… "af0e425… <NA> 1
#> 19 北京医… 39.9 北京市… 北京市 北京… 东城… "" <NA> 1
#> 20 北京卫… 39.9 北京市… 北京市 北京… 东城… <NA> <NA> 1
#> # ℹ 2 more variables: location$lng <dbl>, uid <chr>

然后我们循环解析所有的坐标点。这里为了更高效,我采用的是多线程爬取:

library(parallel)
dir.create("res2")
makeCluster(20) -> cl
clusterExport(cl, "%>%")
clusterExport(cl, "coorddf")
parLapply(cl, 1:nrow(coorddf), fun = function(x){
if(!file.exists(paste0("res2/", coorddf$id[x], "_0.rds"))) {
jsonlite::fromJSON(paste0("https://api.map.baidu.com/place/v2/search?query=医院$诊所$药店$体检$疗养院$急救中心$疾控中心$医疗&tag=医疗&location=", coorddf$Y[x], "," , coorddf$X[x], "&radius=10000&output=json&ak=a8niO5h8icau3IRNG2Wv2k5CduVyrktL&coord_type=wgs84ll&ret_coordtype=gcj02ll&page_size=20&page_num=0")) -> ls
ls %>%
readr::write_rds(paste0("res2/", coorddf$id[x], "_0.rds"))
pagenum <- ls$total %/% 20
if(pagenum >= 1) {
for (p in 1:pagenum) {
jsonlite::fromJSON(paste0("https://api.map.baidu.com/place/v2/search?query=医院$诊所$药店$体检$疗养院$急救中心$疾控中心$医疗&tag=医疗&location=", coorddf$Y[x], "," , coorddf$X[x], "&radius=10000&output=json&ak=a8niO5h8icau3IRNG2Wv2k5CduVyrktL&coord_type=wgs84ll&ret_coordtype=gcj02ll&page_size=20&page_num=", p)) %>%
readr::write_rds(paste0("res2/", coorddf$id[x], "_", p, ".rds"))
}
}
}
}) -> res2

并不是所有的文件都是有数据的,所以我们先删除那些没有数据的:

# 删除没有结果的
parLapply(cl, fs::dir_ls("res2"), function(x){
readr::read_rds(x) -> ls
if(length(ls$results) == 0) {
file.remove(x)
}
})

然后合并有结果的:

# 合并有结果的
parLapply(cl, fs::dir_ls("res2"), function(x){
readr::read_rds(x) -> ls
ls$results %>%
dplyr::as_tibble() %>%
tidyr::unnest(location)
}) %>%
bind_rows() -> df

df %>%
distinct() -> df

df

# 注意返回结果中的坐标是 GCJ02 坐标系的
# GCJ02 转 WGS84
source("坐标转换.R")
df %>%
mutate(经度 = as.numeric(lng),
纬度 = as.numeric(lat),
value2 = map2_chr(经度, 纬度, GCJ02_WGS84)) %>%
select(-contains("度")) %>%
separate(value2, into = c("经度", "纬度"), sep = ",") %>%
type_convert() %>%
select(-lat, -lng) %>%
select(name, lat = 纬度, lng = 经度, everything()) -> df2

df2 %>%
writexl::write_xlsx("全国医疗机构分布(50km范围检索).xlsx")

这样我们就得到了 50km 范围内的解析结果。不过这个结果是不全的,大家有时间可以试试更细致的间距(不过也要主要自己的解析额度限制,可能要分好多天解析)。

附件中我给大家提供了一份 10km 范围内的检索结果:

  • 全国医疗机构分布(10km范围检索).xlsx

全国医疗机构分布地图

下面我们使用这个数据绘制一幅地图展示:

chinacity2020mini 文件夹是我处理的一份带九段线小地图的中国市级行政区划数据。

# 使用 10km 范围内的检索结果
readxl::read_xlsx("全国医疗机构分布(10km范围检索).xlsx") -> df

df %>%
st_as_sf(coords = c("lng", "lat"), crs = 4326) -> dfsf

read_sf("chinacity2020mini/chinacity2020mini.shp") %>%
filter(!str_detect(class, "(比例尺)|(指北针)")) -> citymap
read_sf("chinacity2020mini/chinacity2020mini_line.shp") %>%
filter(!str_detect(class, "(_)|(-)|(胡焕庸线)")) -> citylinemap

citymap %>%
st_simplify(dTolerance = 2000) -> citymapsim
citylinemap %>%
st_simplify(dTolerance = 2000) -> citylinemapsim

unique(citylinemapsim$class)
#> [1] "城市" "九段线" "海岸线" "小地图框格"

# 提取小地图范围中的
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"
small_bbox <- st_bbox(c(xmin = 120000,
xmax = 1766004.1,
ymax = 2557786.0,
ymin = 320000),
crs = st_crs(mycrs)) %>%
st_as_sfc()

dfsf %>%
st_transform(mycrs) %>%
st_intersection(small_bbox) -> dfsf2

# 移动到右下角
dfsf2 %>%
mutate(geometry = geometry * 0.5 + c(2100000, 1665139)) %>%
st_set_crs(mycrs) %>%
st_transform(crs = 4326) -> dfsf2

library(ggspatial)

ggplot(data = citymapsim) +
geom_sf(fill = NA, linewidth = 0.1) +
geom_sf(data = citylinemapsim,
aes(color = factor(class),
linewidth = factor(class)),
show.legend = F) +
scale_color_manual(values = c(
"城市" = "black",
"九段线" = "black",
"海岸线" = "#0055AA",
"小地图框格" = "black"
)) +
scale_linewidth_manual(values = c(
"城市" = 0.1,
"九段线" = 0.3,
"海岸线" = 0.3,
"小地图框格" = 0.3
)) +
ggnewscale::new_scale_color() +
geom_sf(data = bind_rows(dfsf, dfsf2),
aes(color = province), size = 0.01, alpha = 0.5,
show.legend = F) +
scale_color_manual(values = as.character(paletteer::paletteer_d("ggsci::default_igv", n = 37))) +
annotation_scale(location = "bl", width_hint = 0.3,
text_family = "cnfont") +
annotation_north_arrow(location = "tr",
which_north = "false",
pad_x = unit(0.5, "cm"),
pad_y = unit(0.5, "cm"),
style = north_arrow_nautical(
text_family = "cnfont"
)) +
labs(title = "中国各种医院、诊所、药店等医疗机构地理分布",
subtitle = "数据处理&绘图:微信公众号 RStata",
caption = "数据来源:百度地图地点检索接口(10km 范围逐点的检索)") +
theme(plot.background = element_rect(fill = "white",
color = "white")) -> p1

ggsave("中国各种医院、诊所、药店等医疗机构地理分布.png",
width = 10, height = 8, device = png)

各城市医疗机构密度地图

使用 st_area() 可以轻松计算各城市的行政区划面积,不过记得不要使用上面的那个带九段线小地图的数据(被编辑过的)。

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

city %>%
mutate(面积 = st_area(.)) %>%
st_drop_geometry() %>%
mutate(面积 = units::set_units(面积, km2),
面积 = as.numeric(面积)) %>%
select(-contains("类型")) -> cityarea

cityarea
#> # A tibble: 370 × 5
#> 省 省代码 市 市代码 面积
#> <chr> <dbl> <chr> <dbl> <dbl>
#> 1 安徽省 340000 安庆市 340800 13502.
#> 2 安徽省 340000 蚌埠市 340300 5971.
#> 3 安徽省 340000 亳州市 341600 8530.
#> 4 安徽省 340000 池州市 341700 8406.
#> 5 安徽省 340000 滁州市 341100 13524.
#> 6 安徽省 340000 阜阳市 341200 10125.
#> 7 安徽省 340000 合肥市 340100 11483.
#> 8 安徽省 340000 淮北市 340600 2748.
#> 9 安徽省 340000 淮南市 340400 5534.
#> 10 安徽省 340000 黄山市 341000 9684.
#> # ℹ 360 more rows

然后就可以合并数据绘图了:

dfsf %>%
select(name) %>%
st_transform(3055) %>%
st_intersection(st_transform(city, 3055)) %>%
st_drop_geometry() %>%
count(省, 省代码, 市, 市代码) -> countdf

citymapsim %>%
left_join(countdf) %>%
left_join(cityarea) %>%
mutate(density = n / 面积) -> citymapsim2

hist(citymapsim2$density)

ggplot(data = citymapsim2) +
geom_sf(aes(fill = density), linewidth = 0.1) +
geom_sf(data = citylinemapsim,
aes(color = factor(class),
linewidth = factor(class)),
show.legend = F) +
scale_color_manual(values = c(
"城市" = "black",
"九段线" = "black",
"海岸线" = "#0055AA",
"小地图框格" = "black"
)) +
scale_linewidth_manual(values = c(
"城市" = 0.1,
"九段线" = 0.3,
"海岸线" = 0.3,
"小地图框格" = 0.3
)) +
annotation_scale(location = "bl", width_hint = 0.3,
text_family = "cnfont") +
annotation_north_arrow(location = "tr",
which_north = "false",
pad_x = unit(0.5, "cm"),
pad_y = unit(0.5, "cm"),
style = north_arrow_nautical(
text_family = "cnfont"
)) +
labs(title = "各城市医疗机构密度(个/平方千米)",
subtitle = "数据处理&绘图:微信公众号 RStata",
caption = "数据来源:百度地图地点检索接口(10km 范围逐点的检索)",
fill = "医疗机构密度(个/平方千米)") +
theme(plot.background = element_rect(fill = "white",
color = "white"),
legend.position = c(0.15, 0.15),
legend.direction = "horizontal",
legend.key.height = unit(0.3, "cm"),
legend.key.width = unit(1, "cm"),) +
guides(fill = guide_colorsteps(title.position = "top")) -> p2

ggsave("医疗机构密度.png", width = 10, height = 8, device = png)

点击这里跳转到 RStata 短书平台获取附件:使用 R 语言爬取全国医疗机构列表

评论