使用 R 语言处理台风路径数据及判断每天每个区县是否有台风

中国气象局热带气旋资料中心(https://tcdata.typhoon.org.cn/zjljsjj.html)提供了历年中国热带气旋最佳路径数据集。据此我们可以合成每次台风的路径以及判断每次台风经过的省市区县。

原始数据介绍

原始数据是这样的,以 CH2024BST.txt 数据为例:

66666 2401   38 0001 2401 0 6 EWINIAR                            20250301
2024052400 1 83 1283 1004 13
2024052406 1 90 1273 1004 13
......
2024060200 9 448 1738 992 20
2024060206 9 462 1792 992 20
66666 2402 23 0002 2402 0 3 MALIKSI 20250301
2024053006 1 181 1120 1002 13
2024053012 1 183 1120 1000 15
......
2024060203 0 246 1168 1005 10
2024060206 0 243 1171 1005 10
......

比较长的行是头记录,含义如下:

比较短的是热带气旋的最佳路径数据记录,含义如下:

特别需要注意的是这个时间是世界时,也就是 UTC,如果想转换成北京时间(UTC+8)还需要加上 8 个小时。

想要判断每次台风是否经过某个区县。一种简单的方法就是直接判断这些路径坐标是否在该区县内。但是这样可能会忽略中间路径经过的曲线。因此,我们首先应该根据这些坐标点合成热带气旋的路径数据。

热带气旋路径生成

附件中的 data 文件夹就是从上述网址下载的 txt 文件合集。读取合并:

library(tidyverse)
library(sf)
fs::dir_ls("data") %>%
lapply(read_lines) %>%
c() %>%
unlist() %>%
as_tibble() %>%
mutate(len = str_length(value)) -> filedf

filedf

#> # A tibble: 75,888 × 2
#> value len
#> <chr> <int>
#> 1 66666 0000 49 0001 0000 0 6 Carmen 20110… 73
#> 2 1949011300 0 57 1399 1006 0 34
#> 3 1949011306 0 59 1393 1006 0 34
#> 4 1949011312 0 63 1387 1006 0 34
#> 5 1949011318 0 67 1380 1006 0 34
#> 6 1949011400 0 72 1373 1005 0 34
#> 7 1949011406 0 77 1367 1005 0 34
#> 8 1949011412 0 82 1360 1005 0 34
#> 9 1949011418 0 88 1353 1005 0 34
#> 10 1949011500 1 96 1348 1005 15 34
#> # ℹ 75,878 more rows

由于比较长的行是头记录,比较短的是路径数据,所以我们可以根据长度来给每个气旋路径生成一个标志符:

filedf %>%
mutate(z = case_when(len >= 40 ~ row_number())) %>%
fill(z) %>%
filter(len < 40) %>%
select(-len) %>%
separate(value, into = paste0("X", 1:6)) -> filedf2

filedf2

#> # A tibble: 73,371 × 7
#> X1 X2 X3 X4 X5 X6 z
#> <chr> <chr> <chr> <chr> <chr> <chr> <int>
#> 1 1949011300 0 57 1399 1006 0 1
#> 2 1949011306 0 59 1393 1006 0 1
#> 3 1949011312 0 63 1387 1006 0 1
#> 4 1949011318 0 67 1380 1006 0 1
#> 5 1949011400 0 72 1373 1005 0 1
#> 6 1949011406 0 77 1367 1005 0 1
#> 7 1949011412 0 82 1360 1005 0 1
#> 8 1949011418 0 88 1353 1005 0 1
#> 9 1949011500 1 96 1348 1005 15 1
#> 10 1949011506 2 108 1345 1002 20 1
#> # ℹ 73,361 more rows

处理时间和经纬度:

filedf2 %>%
mutate(X1 = lubridate::ymd_h(X1) + hours(8)) %>%
select(id = z, date = X1, lat = X3, lon = X4) %>%
mutate(date = lubridate::date(date)) %>%
type_convert() %>%
mutate(lat = lat / 10, lon = lon / 10) -> df

df

#> # A tibble: 73,371 × 4
#> id date lat lon
#> <int> <date> <dbl> <dbl>
#> 1 1 1949-01-13 5.7 140.
#> 2 1 1949-01-13 5.9 139.
#> 3 1 1949-01-13 6.3 139.
#> 4 1 1949-01-14 6.7 138
#> 5 1 1949-01-14 7.2 137.
#> 6 1 1949-01-14 7.7 137.
#> 7 1 1949-01-14 8.2 136
#> 8 1 1949-01-15 8.8 135.
#> 9 1 1949-01-15 9.6 135.
#> 10 1 1949-01-15 10.8 134.
#> # ℹ 73,361 more rows

然后就是路径合成了,如果气旋有多个路径点,那么就可以合成路线:

df %>%
group_by(id, date) %>%
mutate(n = n()) %>%
ungroup() %>%
filter(n > 1) -> df1

df1

#> # A tibble: 72,423 × 5
#> id date lat lon n
#> <int> <date> <dbl> <dbl> <int>
#> 1 1 1949-01-13 5.7 140. 3
#> 2 1 1949-01-13 5.9 139. 3
#> 3 1 1949-01-13 6.3 139. 3
#> 4 1 1949-01-14 6.7 138 4
#> 5 1 1949-01-14 7.2 137. 4
#> 6 1 1949-01-14 7.7 137. 4
#> 7 1 1949-01-14 8.2 136 4
#> 8 1 1949-01-15 8.8 135. 4
#> 9 1 1949-01-15 9.6 135. 4
#> 10 1 1949-01-15 10.8 134. 4
#> # ℹ 72,413 more rows

如果气旋持续多个日期,那么从第一天到第二天的路径很难判断到底是第一天经过的某个区县,还是第二天经过的。所以这里我选择了分日期生成路径线条:

df1 %>%
nest(-c(id, date)) -> nestdf

nestdf

#> # A tibble: 19,009 × 3
#> id date data
#> <int> <date> <list>
#> 1 1 1949-01-13 <tibble [3 × 3]>
#> 2 1 1949-01-14 <tibble [4 × 3]>
#> 3 1 1949-01-15 <tibble [4 × 3]>
#> 4 1 1949-01-16 <tibble [4 × 3]>
#> 5 1 1949-01-17 <tibble [4 × 3]>
#> 6 1 1949-01-18 <tibble [4 × 3]>
#> 7 1 1949-01-19 <tibble [4 × 3]>
#> 8 1 1949-01-20 <tibble [4 × 3]>
#> 9 1 1949-01-21 <tibble [4 × 3]>
#> 10 1 1949-01-22 <tibble [4 × 3]>
#> # ℹ 18,999 more rows

# 例如第一个线条
nestdf$data[[1]] %>%
select(lon, lat) %>%
as.matrix() %>%
st_linestring() %>%
st_sfc(crs = 4326)

#> Geometry set for 1 feature
#> Geometry type: LINESTRING
#> Dimension: XY
#> Bounding box: xmin: 138.7 ymin: 5.7 xmax: 139.9 ymax: 6.3
#> Geodetic CRS: WGS 84

所有的线条:

nestdf$data[[1]] %>%
select(lon, lat) %>%
as.matrix() %>%
st_linestring() %>%
st_sfc(crs = 4326)

#> Geometry set for 1 feature
#> Geometry type: LINESTRING
#> Dimension: XY
#> Bounding box: xmin: 138.7 ymin: 5.7 xmax: 139.9 ymax: 6.3
#> Geodetic CRS: WGS 84

nestdf %>%
mutate(geometry = map(data, function(x){
x %>%
select(lon, lat) %>%
as.matrix() %>%
st_linestring() %>%
st_sfc(crs = 4326)
})) -> nestdf2

nestdf2

#> # A tibble: 19,009 × 4
#> id date data geometry
#> <int> <date> <list> <list>
#> 1 1 1949-01-13 <tibble [3 × 3]> <LINESTRING (...>
#> 2 1 1949-01-14 <tibble [4 × 3]> <LINESTRING (...>
#> 3 1 1949-01-15 <tibble [4 × 3]> <LINESTRING (...>
#> 4 1 1949-01-16 <tibble [4 × 3]> <LINESTRING (...>
#> 5 1 1949-01-17 <tibble [4 × 3]> <LINESTRING (...>
#> 6 1 1949-01-18 <tibble [4 × 3]> <LINESTRING (...>
#> 7 1 1949-01-19 <tibble [4 × 3]> <LINESTRING (...>
#> 8 1 1949-01-20 <tibble [4 × 3]> <LINESTRING (...>
#> 9 1 1949-01-21 <tibble [4 × 3]> <LINESTRING (...>
#> 10 1 1949-01-22 <tibble [4 × 3]> <LINESTRING (...>
#> # ℹ 18,999 more rows

再转换成 sf 对象:

nestdf2 %>%
select(-data) %>%
unnest(geometry) %>%
st_sf(crs = 4326) -> dfsf

dfsf

#> Simple feature collection with 19009 features and 2 fields
#> Geometry type: LINESTRING
#> Dimension: XY
#> Bounding box: xmin: 95 ymin: 0.5 xmax: 255 ymax: 69.8
#> Geodetic CRS: WGS 84
#> # A tibble: 19,009 × 3
#> id date geometry
#> * <int> <date> <LINESTRING [°]>
#> 1 1 1949-01-13 (139.9 5.7, 139.3 5.9, 138.7 6.3)
#> 2 1 1949-01-14 (138 6.7, 137.3 7.2, 136.7 7.7, 136 8.2)
#> 3 1 1949-01-15 (135.3 8.8, 134.8 9.6, 134.5 10.8, 134.5 11.6)
#> 4 1 1949-01-16 (134.6 12.2, 134.7 12.7, 135.1 13.1, 135.3 13.2)
#> 5 1 1949-01-17 (135.6 13.2, 135.9 13.1, 136.1 12.8, 135.9 12.6)
#> 6 1 1949-01-18 (135.6 12.4, 135.2 12.3, 134.5 12.4, 133.8 12.4)
#> 7 1 1949-01-19 (132.8 12.3, 131.8 12.2, 130.9 12.1, 129.9 11.9)
#> 8 1 1949-01-20 (129.1 11.7, 128.2 11.6, 127.4 11.4, 126.6 11.3)
#> 9 1 1949-01-21 (125.7 11.2, 124.9 11.3, 124.3 11.4, 123.8 11.6)
#> 10 1 1949-01-22 (123.3 11.7, 122.7 11.9, 122.2 12, 121.2 12.5)
#> # ℹ 18,999 more rows

保存:

dfsf %>%
write_rds("台风分日期路径(linestring).rds")

如果路径坐标只有一个,就不必合成了,直接保存即可:

df %>%
group_by(id, date) %>%
mutate(n = n()) %>%
ungroup() %>%
filter(n == 1) %>%
select(-n) %>%
st_as_sf(coords = c("lon", "lat"), crs = 4326) -> dfb

dfb %>%
write_rds("台风分日期路径(point).rds")

最终合成的台风路径是这样的:

mapview::mapview(dfsf)

mapview::mapview(dfb)

判断每天每个省市区县是否有台风

这里我们以区县为例。计算上述的线条和区县矢量数据的相交情况即可:

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"
read_sf("2021行政区划/县.shp") %>%
select(-contains("类型")) %>%
st_transform(mycrs) -> county

dfsf %>%
st_transform(mycrs) -> dfsf

# 长度大于 0 的
dfsf %>%
mutate(len = st_length(.),
len = as.numeric(len)) %>%
filter(len > 0) %>%
st_intersection(county) -> resdf1

resdf1

#> Simple feature collection with 9600 features and 9 fields
#> Geometry type: GEOMETRY
#> Dimension: XY
#> Bounding box: xmin: -532224 ymin: 576096.5 xmax: 2203948 ymax: 5843852
#> Projected CRS: +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
#> # A tibble: 9,600 × 10
#> id date len 省 省代码 市 市代码 县 县代码
#> * <int> <date> <dbl> <chr> <dbl> <chr> <dbl> <chr> <dbl>
#> 1 4294 1953-07-05 456742. 安徽省 340000 安庆市 340800 大观区 340803
#> 2 64264 2012-08-10 112105. 安徽省 340000 安庆市 340800 大观区 340803
#> 3 23896 1969-09-29 471868. 安徽省 340000 安庆市 340800 怀宁县 340822
#> 4 29801 1974-08-13 362856. 安徽省 340000 安庆市 340800 怀宁县 340822
#> 5 54194 1999-08-25 625833. 安徽省 340000 安庆市 340800 怀宁县 340822
#> 6 64264 2012-08-10 112105. 安徽省 340000 安庆市 340800 怀宁县 340822
#> 7 67121 2015-08-10 471402. 安徽省 340000 安庆市 340800 怀宁县 340822
#> 8 23896 1969-09-29 471868. 安徽省 340000 安庆市 340800 潜山市 340882
#> 9 29801 1974-08-13 362856. 安徽省 340000 安庆市 340800 潜山市 340882
#> 10 51156 1995-08-01 493449. 安徽省 340000 安庆市 340800 潜山市 340882
#> # ℹ 9,590 more rows
#> # ℹ 1 more variable: geometry <GEOMETRY [m]>

# 长度等于 0 的(点)

dfsf %>%
mutate(len = st_length(.),
len = as.numeric(len)) %>%
filter(len == 0) %>%
st_cast("POINT") -> dfsf2

dfsf2 %>%
st_intersection(county) -> resdf2

bind_rows(
resdf1 %>%
st_drop_geometry() %>%
select(date, 省, 省代码, 市, 市代码, 县, 县代码),
resdf2 %>%
st_drop_geometry() %>%
select(date, 省, 省代码, 市, 市代码, 县, 县代码)
) -> resdf

resdf %>%
distinct() -> resdf

resdf

#> # A tibble: 9,599 × 7
#> date 省 省代码 市 市代码 县 县代码
#> <date> <chr> <dbl> <chr> <dbl> <chr> <dbl>
#> 1 1953-07-05 安徽省 340000 安庆市 340800 大观区 340803
#> 2 2012-08-10 安徽省 340000 安庆市 340800 大观区 340803
#> 3 1969-09-29 安徽省 340000 安庆市 340800 怀宁县 340822
#> 4 1974-08-13 安徽省 340000 安庆市 340800 怀宁县 340822
#> 5 1999-08-25 安徽省 340000 安庆市 340800 怀宁县 340822
#> 6 2012-08-10 安徽省 340000 安庆市 340800 怀宁县 340822
#> 7 2015-08-10 安徽省 340000 安庆市 340800 怀宁县 340822
#> 8 1969-09-29 安徽省 340000 安庆市 340800 潜山市 340882
#> 9 1974-08-13 安徽省 340000 安庆市 340800 潜山市 340882
#> 10 1995-08-01 安徽省 340000 安庆市 340800 潜山市 340882
#> # ℹ 9,589 more rows

如果想要生成平衡面板,可以先准备一个平衡面板:

(ymd("1949-01-01") + 0:(ymd("2024-12-31") - ymd("1949-01-01"))) %>%
as_tibble() %>%
crossing(
county %>%
st_drop_geometry()
) %>%
rename(date = value) -> crossdf
crossdf

#> # A tibble: 79,862,643 × 7
#> date 省 省代码 市 市代码 县 县代码
#> <date> <chr> <dbl> <chr> <dbl> <chr> <dbl>
#> 1 1949-01-01 上海市 310000 上海市 310000 嘉定区 310114
#> 2 1949-01-01 上海市 310000 上海市 310000 奉贤区 310120
#> 3 1949-01-01 上海市 310000 上海市 310000 宝山区 310113
#> 4 1949-01-01 上海市 310000 上海市 310000 崇明区 310151
#> 5 1949-01-01 上海市 310000 上海市 310000 徐汇区 310104
#> 6 1949-01-01 上海市 310000 上海市 310000 普陀区 310107
#> 7 1949-01-01 上海市 310000 上海市 310000 杨浦区 310110
#> 8 1949-01-01 上海市 310000 上海市 310000 松江区 310117
#> 9 1949-01-01 上海市 310000 上海市 310000 浦东新区 310115
#> 10 1949-01-01 上海市 310000 上海市 310000 虹口区 310109
#> # ℹ 79,862,633 more rows

然后再和前面的数据连接即可:

crossdf %>%
left_join(
resdf %>%
mutate(是否有台风 = 1)
) -> resdfn

resdfn

#> # A tibble: 79,862,643 × 8
#> date 省 省代码 市 市代码 县 县代码 是否有台风
#> <date> <chr> <dbl> <chr> <dbl> <chr> <dbl> <dbl>
#> 1 1949-01-01 上海市 310000 上海市 310000 嘉定区 310114 NA
#> 2 1949-01-01 上海市 310000 上海市 310000 奉贤区 310120 NA
#> 3 1949-01-01 上海市 310000 上海市 310000 宝山区 310113 NA
#> 4 1949-01-01 上海市 310000 上海市 310000 崇明区 310151 NA
#> 5 1949-01-01 上海市 310000 上海市 310000 徐汇区 310104 NA
#> 6 1949-01-01 上海市 310000 上海市 310000 普陀区 310107 NA
#> 7 1949-01-01 上海市 310000 上海市 310000 杨浦区 310110 NA
#> 8 1949-01-01 上海市 310000 上海市 310000 松江区 310117 NA
#> 9 1949-01-01 上海市 310000 上海市 310000 浦东新区 310115 NA
#> 10 1949-01-01 上海市 310000 上海市 310000 虹口区 310109 NA
#> # ℹ 79,862,633 more rows

最后保存数据:

resdfn %>%
mutate(是否有台风 = if_else(is.na(是否有台风), 0, 是否有台风)) %>%
rename(日期 = date) %>%
haven::write_dta("1949~2024年各区县每天是否有台风.dta")

# 如果只保留有台风的天
resdfn %>%
mutate(是否有台风 = if_else(is.na(是否有台风), 0, 是否有台风)) %>%
rename(日期 = date) %>%
filter(是否有台风 == 1) %>%
haven::write_dta("1949~2024年各区县每天是否有台风(有台风的天).dta")

点击这里跳转到 RStata 短书平台获取附件:使用 R 语言处理台风路径数据及判断每天每个区县是否有台风

评论